Repository: aclindsa/ofxgo Branch: master Commit: b2d1132f5987 Files: 57 Total size: 574.2 KB Directory structure: gitextract_ipheoeo0/ ├── .github/ │ └── workflows/ │ └── test.yml ├── LICENSE ├── README.md ├── bank.go ├── bank_test.go ├── basic_client.go ├── basic_client_test.go ├── client.go ├── cmd/ │ └── ofx/ │ ├── bankdownload.go │ ├── banktransactions.go │ ├── ccdownload.go │ ├── cctransactions.go │ ├── command.go │ ├── detect_settings.go │ ├── get_accounts.go │ ├── invdownload.go │ ├── invtransactions.go │ ├── main.go │ ├── profiledownload.go │ └── util.go ├── common.go ├── common_test.go ├── constants.go ├── constants_test.go ├── creditcard.go ├── creditcard_test.go ├── discovercard_client.go ├── doc.go ├── generate_constants.py ├── go.mod ├── go.sum ├── invstmt.go ├── invstmt_test.go ├── leaf_elements.go ├── profile.go ├── profile_test.go ├── request.go ├── request_test.go ├── response.go ├── response_test.go ├── samples/ │ ├── busted_responses/ │ │ ├── bmo_v102__no_header_newline.qfx │ │ └── wellsfargo.qfx │ └── valid_responses/ │ ├── 401k_v203.ofx │ ├── inv_v202.ofx │ ├── ira_v202.ofx │ ├── moneymrkt1_v103.ofx │ ├── moneymrkt1_v103_TYPE1.ofx │ └── moneymrkt1_v203.ofx ├── seclist.go ├── signon.go ├── signon_test.go ├── signup.go ├── signup_test.go ├── types.go ├── types_test.go ├── util.go └── vanguard_client.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/test.yml ================================================ name: ofxgo CI Test on: [push, pull_request] jobs: test: strategy: matrix: go-version: [1.19.x, 1.22.x, 1.24.x] os: [ubuntu-latest, macos-latest, windows-latest] include: - os: ubuntu-latest go-version: 1.14.x - os: windows-latest go-version: 1.14.x runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v5 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 ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================ # OFXGo [![Go Report Card](https://goreportcard.com/badge/github.com/aclindsa/ofxgo)](https://goreportcard.com/report/github.com/aclindsa/ofxgo) [![Build Status](https://github.com/aclindsa/ofxgo/workflows/ofxgo%20CI%20Test/badge.svg?branch=master)](https://github.com/aclindsa/ofxgo/actions?query=workflow%3A%22ofxgo+CI+Test%22+branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/aclindsa/ofxgo/badge.svg?branch=master)](https://coveralls.io/github/aclindsa/ofxgo?branch=master) [![PkgGoDev](https://pkg.go.dev/badge/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 also provides an example command-line client to demonstrate the use of the library. ## Goals 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 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 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 finance software in Go (as it was created to allow me to fetch OFX transactions for my own project, [MoneyGo](https://github.com/aclindsa/moneygo)). Because the OFX specification is rather... 'comprehensive,' it can be difficult for those unfamiliar with it to figure out where to start. To that end, I have 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 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 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 financial institutions, which frequently have 'quirks' in their implementations. The command-line client can be found in the [cmd/ofx directory](https://github.com/aclindsa/ofxgo/tree/master/cmd/ofx) of this repository. ## Library documentation Documentation can be found with the `go doc` tool, or at 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 OFXGo requires go >= 1.12 ## Using the command-line client To install the command-line client and test it out, you may do the following: $ go get -v github.com/aclindsa/ofxgo/cmd/ofx && go install -v github.com/aclindsa/ofxgo/cmd/ofx Once installed (at ~/go/bin/ofx by default, if you haven't set $GOPATH), the command's usage should help you to use it (`./ofx --help` for a listing of the available subcommands and their purposes, `./ofx subcommand --help` for individual subcommand usage). ================================================ FILE: bank.go ================================================ package ofxgo import ( "errors" "github.com/aclindsa/xml" ) // StatementRequest represents a request for a bank statement. It is used to // request balances and/or transactions for checking, savings, money market, // and line of credit accounts. See CCStatementRequest for the analog for // credit card accounts. type StatementRequest struct { XMLName xml.Name `xml:"STMTTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` BankAcctFrom BankAcct `xml:"STMTRQ>BANKACCTFROM"` DtStart *Date `xml:"STMTRQ>INCTRAN>DTSTART,omitempty"` DtEnd *Date `xml:"STMTRQ>INCTRAN>DTEND,omitempty"` Include Boolean `xml:"STMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance) IncludePending Boolean `xml:"STMTRQ>INCLUDEPENDING,omitempty"` // Include pending transactions IncTranImg Boolean `xml:"STMTRQ>INCTRANIMG,omitempty"` // Include transaction images } // Name returns the name of the top-level transaction XML/SGML element func (r *StatementRequest) Name() string { return "STMTTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *StatementRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } if r.IncludePending && version < OfxVersion220 { return false, errors.New("StatementRequest.IncludePending invalid for OFX < 2.2") } if r.IncTranImg && version < OfxVersion210 { return false, errors.New("StatementRequest.IncTranImg invalid for OFX < 2.1") } return r.BankAcctFrom.Valid() } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *StatementRequest) Type() messageType { return BankRq } // Payee specifies a complete billing address for a payee type Payee struct { XMLName xml.Name `xml:"PAYEE"` Name String `xml:"NAME"` Addr1 String `xml:"ADDR1"` Addr2 String `xml:"ADDR2,omitempty"` Addr3 String `xml:"ADDR3,omitempty"` City String `xml:"CITY"` State String `xml:"STATE"` PostalCode String `xml:"POSTALCODE"` Country String `xml:"COUNTRY,omitempty"` Phone String `xml:"PHONE"` } // Valid returns (true, nil) if this struct is valid OFX func (p Payee) Valid() (bool, error) { if len(p.Name) == 0 { return false, errors.New("Payee.Name empty") } else if len(p.Addr1) == 0 { return false, errors.New("Payee.Addr1 empty") } else if len(p.City) == 0 { return false, errors.New("Payee.City empty") } else if len(p.State) == 0 { return false, errors.New("Payee.State empty") } else if len(p.PostalCode) == 0 { return false, errors.New("Payee.PostalCode empty") } else if len(p.Country) != 0 && len(p.Country) != 3 { return false, errors.New("Payee.Country invalid length") } else if len(p.Phone) == 0 { return false, errors.New("Payee.Phone empty") } return true, nil } // ImageData represents the metadata surrounding a check or other image file, // including how to retrieve the image type ImageData struct { XMLName xml.Name `xml:"IMAGEDATA"` ImageType imageType `xml:"IMAGETYPE"` // One of STATEMENT, TRANSACTION, TAX ImageRef String `xml:"IMAGEREF"` // URL or identifier, depending on IMAGEREFTYPE ImageRefType imageRefType `xml:"IMAGEREFTYPE"` // One of OPAQUE, URL, FORMURL (see spec for more details on how to access images of each of these types) // Only one of the next two should be valid at any given time ImageDelay Int `xml:"IMAGEDELAY,omitempty"` // Number of calendar days from DTSERVER (for statement images) or DTPOSTED (for transaction image) the image will become available DtImageAvail *Date `xml:"DTIMAGEAVAIL,omitempty"` // Date image will become available ImageTTL Int `xml:"IMAGETTL,omitempty"` // Number of days after image becomes available that it will remain available CheckSup checkSup `xml:"CHECKSUP,omitempty"` // What is contained in check images. One of FRONTONLY, BACKONLY, FRONTANDBACK } // Transaction represents a single banking transaction. At a minimum, it // identifies the type of transaction (TrnType) and the date it was posted // (DtPosted). Ideally it also provides metadata to help the user recognize // this transaction (i.e. CheckNum, Name or Payee, Memo, etc.) type Transaction struct { XMLName xml.Name `xml:"STMTTRN"` TrnType trnType `xml:"TRNTYPE"` // One of CREDIT, DEBIT, INT (interest earned or paid. Note: Depends on signage of amount), DIV, FEE, SRVCHG (service charge), DEP (deposit), ATM (Note: Depends on signage of amount), POS (Note: Depends on signage of amount), XFER, CHECK, PAYMENT, CASH, DIRECTDEP, DIRECTDEBIT, REPEATPMT, OTHER DtPosted Date `xml:"DTPOSTED"` DtUser *Date `xml:"DTUSER,omitempty"` DtAvail *Date `xml:"DTAVAIL,omitempty"` TrnAmt Amount `xml:"TRNAMT"` FiTID String `xml:"FITID"` // Client uses FITID to detect whether it has previously downloaded the transaction CorrectFiTID String `xml:"CORRECTFITID,omitempty"` // Transaction ID that this transaction corrects, if present CorrectAction correctAction `xml:"CORRECTACTION,omitempty"` // One of DELETE, REPLACE SrvrTID String `xml:"SRVRTID,omitempty"` CheckNum String `xml:"CHECKNUM,omitempty"` RefNum String `xml:"REFNUM,omitempty"` SIC Int `xml:"SIC,omitempty"` // Standard Industrial Code PayeeID String `xml:"PAYEEID,omitempty"` // Note: Servers should provide NAME or PAYEE, but not both Name String `xml:"NAME,omitempty"` Payee *Payee `xml:"PAYEE,omitempty"` ExtdName String `xml:"EXTDNAME,omitempty"` // Extended name of payee or transaction description BankAcctTo *BankAcct `xml:"BANKACCTTO,omitempty"` // If the transfer was to a bank account we have the account information for CCAcctTo *CCAcct `xml:"CCACCTTO,omitempty"` // If the transfer was to a credit card account we have the account information for Memo String `xml:"MEMO,omitempty"` // Extra information (not in NAME) ImageData []ImageData `xml:"IMAGEDATA,omitempty"` // 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 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.) } // Valid returns (true, nil) if this struct is valid OFX func (t Transaction) Valid(version ofxVersion) (bool, error) { var emptyDate Date if !t.TrnType.Valid() || t.TrnType == TrnTypeHold { return false, errors.New("Transaction.TrnType invalid") } else if t.DtPosted.Equal(emptyDate) { return false, errors.New("Transaction.DtPosted not filled") } else if len(t.FiTID) == 0 { return false, errors.New("Transaction.FiTID empty") } else if len(t.CorrectFiTID) > 0 && t.CorrectAction.Valid() { return false, errors.New("Transaction.CorrectFiTID nonempty but CorrectAction invalid") } else if len(t.Name) > 0 && t.Payee != nil { return false, errors.New("Only one of Transaction.Name and Payee may be specified") } if t.Payee != nil { if ok, err := t.Payee.Valid(); !ok { return false, err } } if t.BankAcctTo != nil && t.CCAcctTo != nil { return false, errors.New("Only one of Transaction.BankAcctTo and CCAcctTo may be specified") } else if t.BankAcctTo != nil { if ok, err := t.BankAcctTo.Valid(); !ok { return false, err } } else if t.CCAcctTo != nil { if ok, err := t.CCAcctTo.Valid(); !ok { return false, err } } if version < OfxVersion220 && len(t.ImageData) > 0 { return false, errors.New("Transaction.ImageData only supportd for OFX > 220") } else if len(t.ImageData) > 2 { return false, errors.New("Only 2 of ImageData allowed in Transaction") } var ok1, ok2 bool if t.Currency != nil { ok1, _ = t.Currency.Valid() } if t.OrigCurrency != nil { ok2, _ = t.OrigCurrency.Valid() } if ok1 && ok2 { return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed") } return true, nil } // TransactionList represents a list of bank transactions, and also includes // the date range its transactions cover. type TransactionList struct { XMLName xml.Name `xml:"BANKTRANLIST"` DtStart Date `xml:"DTSTART"` // Start date for transaction data DtEnd Date `xml:"DTEND"` // Value that client should send in next request to ensure that it does not miss any transactions Transactions []Transaction `xml:"STMTTRN,omitempty"` } // Valid returns (true, nil) if this struct is valid OFX func (l TransactionList) Valid(version ofxVersion) (bool, error) { var emptyDate Date if l.DtStart.Equal(emptyDate) { return false, errors.New("TransactionList.DtStart not filled") } else if l.DtEnd.Equal(emptyDate) { return false, errors.New("TransactionList.DtEnd not filled") } for _, t := range l.Transactions { if ok, err := t.Valid(version); !ok { return false, err } } return true, nil } // PendingTransaction represents a single pending transaction. It is similar to // Transaction, but is not finalized (and may never be). For instance, it lacks // FiTID and DtPosted fields. type PendingTransaction struct { XMLName xml.Name `xml:"STMTTRNP"` TrnType trnType `xml:"TRNTYPE"` // One of CREDIT, DEBIT, INT (interest earned or paid. Note: Depends on signage of amount), DIV, FEE, SRVCHG (service charge), DEP (deposit), ATM (Note: Depends on signage of amount), POS (Note: Depends on signage of amount), XFER, CHECK, PAYMENT, CASH, DIRECTDEP, DIRECTDEBIT, REPEATPMT, HOLD, OTHER DtTran Date `xml:"DTTRAN"` DtExpire *Date `xml:"DTEXPIRE,omitempty"` // only valid for TrnType==HOLD, the date the hold will expire TrnAmt Amount `xml:"TRNAMT"` RefNum String `xml:"REFNUM,omitempty"` Name String `xml:"NAME,omitempty"` ExtdName String `xml:"EXTDNAME,omitempty"` // Extended name of payee or transaction description Memo String `xml:"MEMO,omitempty"` // Extra information (not in NAME) ImageData []ImageData `xml:"IMAGEDATA,omitempty"` // 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 OrigCurrency Currency `xml:"ORIGCURRENCY,omitempty"` // Represents the currency TrnAmt was converted to STMTRS' CURDEF from if Valid } // Valid returns (true, nil) if this struct is valid OFX func (t PendingTransaction) Valid() (bool, error) { var emptyDate Date if !t.TrnType.Valid() { return false, errors.New("PendingTransaction.TrnType invalid") } else if t.DtTran.Equal(emptyDate) { return false, errors.New("PendingTransaction.DtTran not filled") } else if len(t.Name) == 0 { return false, errors.New("PendingTransaction.Name empty") } ok1, _ := t.Currency.Valid() ok2, _ := t.OrigCurrency.Valid() if ok1 && ok2 { return false, errors.New("Currency and OrigCurrency both supplied for Pending Transaction, only one allowed") } return true, nil } // PendingTransactionList represents a list of pending transactions, along with // the date they were generated type PendingTransactionList struct { XMLName xml.Name `xml:"BANKTRANLISTP"` DtAsOf Date `xml:"DTASOF"` // Date and time this set of pending transactions was generated Transactions []PendingTransaction `xml:"STMTTRNP,omitempty"` } // Valid returns (true, nil) if this struct is valid OFX func (l PendingTransactionList) Valid() (bool, error) { var emptyDate Date if l.DtAsOf.Equal(emptyDate) { return false, errors.New("PendingTransactionList.DtAsOf not filled") } for _, t := range l.Transactions { if ok, err := t.Valid(); !ok { return false, err } } return true, nil } // Balance represents a generic (free-form) balance defined by an FI. type Balance struct { XMLName xml.Name `xml:"BAL"` Name String `xml:"NAME"` Desc String `xml:"DESC"` // Balance type: // DOLLAR = dollar (value formatted DDDD.cc) // PERCENT = percentage (value formatted XXXX.YYYY) // NUMBER = number (value formatted as is) BalType balType `xml:"BALTYPE"` Value Amount `xml:"VALUE"` DtAsOf *Date `xml:"DTASOF,omitempty"` Currency *Currency `xml:"CURRENCY,omitempty"` // if BALTYPE is DOLLAR } // Valid returns (true, nil) if this struct is valid OFX func (b Balance) Valid() (bool, error) { if len(b.Name) == 0 || len(b.Desc) == 0 { return false, errors.New("Balance Name and Desc not supplied") } if !b.BalType.Valid() { return false, errors.New("Balance BALTYPE not specified") } return true, nil } // StatementResponse represents a bank account statement, including its // balances and possibly transactions. It is a response to StatementRequest, or // sometimes provided as part of an OFX file downloaded manually from an FI. type StatementResponse struct { XMLName xml.Name `xml:"STMTTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` CurDef CurrSymbol `xml:"STMTRS>CURDEF"` BankAcctFrom BankAcct `xml:"STMTRS>BANKACCTFROM"` BankTranList *TransactionList `xml:"STMTRS>BANKTRANLIST,omitempty"` BankTranListP *PendingTransactionList `xml:"STMTRS>BANKTRANLISTP,omitempty"` BalAmt Amount `xml:"STMTRS>LEDGERBAL>BALAMT"` DtAsOf Date `xml:"STMTRS>LEDGERBAL>DTASOF"` AvailBalAmt *Amount `xml:"STMTRS>AVAILBAL>BALAMT,omitempty"` AvailDtAsOf *Date `xml:"STMTRS>AVAILBAL>DTASOF,omitempty"` CashAdvBalAmt *Amount `xml:"STMTRS>CASHADVBALAMT,omitempty"` // Only for CREDITLINE accounts, available balance for cash advances IntRate *Amount `xml:"STMTRS>INTRATE,omitempty"` // Current interest rate BalList []Balance `xml:"STMTRS>BALLIST>BAL,omitempty"` MktgInfo String `xml:"STMTRS>MKTGINFO,omitempty"` // Marketing information } // Name returns the name of the top-level transaction XML/SGML element func (sr *StatementResponse) Name() string { return "STMTTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (sr *StatementResponse) Valid(version ofxVersion) (bool, error) { var emptyDate Date if ok, err := sr.TrnUID.Valid(); !ok { return false, err } else if ok, err := sr.Status.Valid(); !ok { return false, err } else if ok, err := sr.CurDef.Valid(); !ok { return false, err } else if ok, err := sr.BankAcctFrom.Valid(); !ok { return false, err } else if sr.DtAsOf.Equal(emptyDate) { return false, errors.New("StatementResponse.DtAsOf not filled") } else if (sr.AvailBalAmt == nil) != (sr.AvailDtAsOf == nil) { return false, errors.New("StatementResponse.Avail* must both either be present or absent") } if sr.BankTranList != nil { if ok, err := sr.BankTranList.Valid(version); !ok { return false, err } } if sr.BankTranListP != nil { if version < OfxVersion220 { return false, errors.New("StatementResponse.BankTranListP invalid for OFX < 2.2") } if ok, err := sr.BankTranListP.Valid(); !ok { return false, err } } for _, bal := range sr.BalList { if ok, err := bal.Valid(); !ok { return false, err } } return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (sr *StatementResponse) Type() messageType { return BankRs } ================================================ FILE: bank_test.go ================================================ package ofxgo import ( "strings" "testing" "time" ) func TestMarshalBankStatementRequest(t *testing.T) { var expectedString string = ` 20060115112300.000[-5:EST] myusername Pa$$word ENG BNK 1987 OFXGO 0001 123 318398732 78346129 CHECKING Y ` var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "myusername" request.Signon.UserPass = "Pa$$word" request.Signon.Org = "BNK" request.Signon.Fid = "1987" statementRequest := StatementRequest{ TrnUID: "123", BankAcctFrom: BankAcct{ BankID: "318398732", AcctID: "78346129", AcctType: AcctTypeChecking, }, Include: true, } request.Bank = append(request.Bank, &statementRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() EST := time.FixedZone("EST", -5*60*60) request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST) marshalCheckRequest(t, &request, expectedString) } func TestMarshalBankStatementRequest103(t *testing.T) { var expectedString string = `OFXHEADER:100 DATA:OFXSGML VERSION:103 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 20060115112300.000[-5:EST] myusername Pa$$word ENG BNK 1987 OFXGO 0001 123 318398732 78346129 CHECKING Y ` var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion103, } var request Request request.Signon.UserID = "myusername" request.Signon.UserPass = "Pa$$word" request.Signon.Org = "BNK" request.Signon.Fid = "1987" statementRequest := StatementRequest{ TrnUID: "123", BankAcctFrom: BankAcct{ BankID: "318398732", AcctID: "78346129", AcctType: AcctTypeChecking, }, Include: true, } request.Bank = append(request.Bank, &statementRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() EST := time.FixedZone("EST", -5*60*60) request.Signon.DtClient = *NewDate(2006, 1, 15, 11, 23, 0, 0, EST) marshalCheckRequest(t, &request, expectedString) } func TestUnmarshalBankStatementResponse(t *testing.T) { responseReader := strings.NewReader(` 0 INFO 20060115112303 ENG 20050221091300 20060102160000 BNK 1987 1001 0 INFO USD 318398732 78346129 CHECKING 20060101 20060115 CHECK 20060104 -200.00 00592 2002 ATM 20060112 20060112 -300.00 00679 200.29 200601141600 200.29 200601141600 `) var expected Response expected.Version = OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0) expected.Signon.Language = "ENG" expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0) expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0) expected.Signon.Org = "BNK" expected.Signon.Fid = "1987" var trnamt1, trnamt2 Amount trnamt1.SetFrac64(-20000, 100) trnamt2.SetFrac64(-30000, 100) banktranlist := TransactionList{ DtStart: *NewDateGMT(2006, 1, 1, 0, 0, 0, 0), DtEnd: *NewDateGMT(2006, 1, 15, 0, 0, 0, 0), Transactions: []Transaction{ { TrnType: TrnTypeCheck, DtPosted: *NewDateGMT(2006, 1, 4, 0, 0, 0, 0), TrnAmt: trnamt1, FiTID: "00592", CheckNum: "2002", }, { TrnType: TrnTypeATM, DtPosted: *NewDateGMT(2006, 1, 12, 0, 0, 0, 0), DtUser: NewDateGMT(2006, 1, 12, 0, 0, 0, 0), TrnAmt: trnamt2, FiTID: "00679", }, }, } var balamt, availbalamt Amount balamt.SetFrac64(20029, 100) availbalamt.SetFrac64(20029, 100) usd, err := NewCurrSymbol("USD") if err != nil { t.Fatalf("Unexpected error creating CurrSymbol for USD\n") } statementResponse := StatementResponse{ TrnUID: "1001", Status: Status{ Code: 0, Severity: "INFO", }, CurDef: *usd, BankAcctFrom: BankAcct{ BankID: "318398732", AcctID: "78346129", AcctType: AcctTypeChecking, }, BankTranList: &banktranlist, BalAmt: balamt, DtAsOf: *NewDateGMT(2006, 1, 14, 16, 0, 0, 0), AvailBalAmt: &availbalamt, AvailDtAsOf: NewDateGMT(2006, 1, 14, 16, 0, 0, 0), } expected.Bank = append(expected.Bank, &statementResponse) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } 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") } } ================================================ FILE: basic_client.go ================================================ 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 HTTPClient *http.Client } // 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") } httpClient := c.HTTPClient if httpClient == nil { httpClient = http.DefaultClient } 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 := httpClient.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) } ================================================ FILE: basic_client_test.go ================================================ package ofxgo import ( "errors" "net" "net/http" "strings" "testing" ) func TestBasicClient_HTTPClient(t *testing.T) { c := &BasicClient{ HTTPClient: &http.Client{ Transport: &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { return nil, errors.New("bad test client") }, }, }, } _, err := c.Request(&Request{ URL: "https://test", Signon: SignonRequest{ UserID: "test", UserPass: "test", }, }) if err == nil || !strings.Contains(err.Error(), "bad test client") { t.Fatalf("expected error containing 'bad test client', got: %v", err) } } ================================================ FILE: client.go ================================================ package ofxgo import ( "io" "net/http" "strings" ) // 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 also provides the Request and RequestNoParse helper methods to aid in // making and parsing requests. type Client interface { // Used to fill out a Request object OfxVersion() ofxVersion ID() String Version() String IndentRequests() bool CarriageReturnNewLines() bool // 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 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) // 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) RequestNoParse(r *Request) (*http.Response, error) // RawRequest is little more than a thin wrapper around http.Post // // 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 yourself (perhaps for downloading an OFX file for use by an // external program, or debugging server behavior), or have a handcrafted // request you'd like to try. // // Caveats: RawRequest does *not* take client settings into account as // Client.Request() does, so your particular server may or may not like // whatever we read from 'r'. The caller is responsible for closing the // http Response.Body (see the http module's documentation for more // information) RawRequest(URL string, r io.Reader) (*http.Response, error) } type clientCreationFunc func(*BasicClient) Client // 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 } // clientRequestNoParse can be used for building clients' RequestNoParse // methods if they require fairly standard behavior func clientRequestNoParse(c Client, r *Request) (*http.Response, error) { r.SetClientFields(c) b, err := r.Marshal() if err != nil { return nil, err } return c.RawRequest(r.URL, b) } // clientRequest can be used for building clients' Request methods if they // require fairly standard behavior func clientRequest(c Client, r *Request) (*Response, error) { response, err := c.RequestNoParse(r) if err != nil { return nil, err } defer response.Body.Close() ofxresp, err := ParseResponse(response.Body) if err != nil { return nil, err } return ofxresp, nil } ================================================ FILE: cmd/ofx/bankdownload.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "io" "os" ) var downloadCommand = command{ Name: "download-bank", Description: "Download a bank account statement to a file", Flags: flag.NewFlagSet("download-bank", flag.ExitOnError), CheckFlags: downloadCheckFlags, Do: download, } var filename, bankID, acctID, acctType string func init() { defineServerFlags(downloadCommand.Flags) downloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to") downloadCommand.Flags.StringVar(&bankID, "bankid", "", "BankID (from `get-accounts` subcommand)") downloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") downloadCommand.Flags.StringVar(&acctType, "accttype", "CHECKING", "AcctType (from `get-accounts` subcommand)") } func downloadCheckFlags() bool { ret := checkServerFlags() if len(filename) == 0 { fmt.Println("Error: Filename empty") return false } return ret } func download() { client, query := newRequest() acctTypeEnum, err := ofxgo.NewAcctType(acctType) if err != nil { fmt.Println("Error parsing accttype:", err) os.Exit(1) } uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.StatementRequest{ TrnUID: *uid, BankAcctFrom: ofxgo.BankAcct{ BankID: ofxgo.String(bankID), AcctID: ofxgo.String(acctID), AcctType: acctTypeEnum, }, Include: true, } query.Bank = append(query.Bank, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.RequestNoParse(query) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } defer response.Body.Close() file, err := os.Create(filename) if err != nil { fmt.Println("Error creating file to write to:", err) os.Exit(1) } defer file.Close() _, err = io.Copy(file, response.Body) if err != nil { fmt.Println("Error writing response to file:", err) os.Exit(1) } } ================================================ FILE: cmd/ofx/banktransactions.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "os" ) var bankTransactionsCommand = command{ Name: "transactions-bank", Description: "Print bank transactions and balance", Flags: flag.NewFlagSet("transactions-bank", flag.ExitOnError), CheckFlags: checkServerFlags, Do: bankTransactions, } func init() { defineServerFlags(bankTransactionsCommand.Flags) bankTransactionsCommand.Flags.StringVar(&bankID, "bankid", "", "BankID (from `get-accounts` subcommand)") bankTransactionsCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") bankTransactionsCommand.Flags.StringVar(&acctType, "accttype", "CHECKING", "AcctType (from `get-accounts` subcommand)") } func bankTransactions() { client, query := newRequest() acctTypeEnum, err := ofxgo.NewAcctType(acctType) if err != nil { fmt.Println("Error parsing accttype:", err) os.Exit(1) } uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.StatementRequest{ TrnUID: *uid, BankAcctFrom: ofxgo.BankAcct{ BankID: ofxgo.String(bankID), AcctID: ofxgo.String(acctID), AcctType: acctTypeEnum, }, Include: true, } query.Bank = append(query.Bank, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.Request(query) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } 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") return } 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 { printTransaction(stmt.CurDef, &tran) } } } func printTransaction(defCurrency ofxgo.CurrSymbol, tran *ofxgo.Transaction) { currency := defCurrency if tran.Currency != nil { currency = tran.Currency.CurSym } var name string if len(tran.Name) > 0 { name = string(tran.Name) } else if tran.Payee != nil { name = string(tran.Payee.Name) } if len(tran.Memo) > 0 { name = name + " - " + string(tran.Memo) } fmt.Printf("%s %-15s %-11s %s\n", tran.DtPosted, tran.TrnAmt.String()+" "+currency.String(), tran.TrnType, name) } ================================================ FILE: cmd/ofx/ccdownload.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "io" "os" ) var ccDownloadCommand = command{ Name: "download-cc", Description: "Download a credit card account statement to a file", Flags: flag.NewFlagSet("download-cc", flag.ExitOnError), CheckFlags: ccDownloadCheckFlags, Do: ccDownload, } func init() { defineServerFlags(ccDownloadCommand.Flags) ccDownloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to") ccDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") } func ccDownloadCheckFlags() bool { ret := checkServerFlags() if len(filename) == 0 { fmt.Println("Error: Filename empty") return false } return ret } func ccDownload() { client, query := newRequest() uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.CCStatementRequest{ TrnUID: *uid, CCAcctFrom: ofxgo.CCAcct{ AcctID: ofxgo.String(acctID), }, Include: true, } query.CreditCard = append(query.CreditCard, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.RequestNoParse(query) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } defer response.Body.Close() file, err := os.Create(filename) if err != nil { fmt.Println("Error creating file to write to:", err) os.Exit(1) } defer file.Close() _, err = io.Copy(file, response.Body) if err != nil { fmt.Println("Error writing response to file:", err) os.Exit(1) } } ================================================ FILE: cmd/ofx/cctransactions.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "os" ) var ccTransactionsCommand = command{ Name: "transactions-cc", Description: "Print credit card transactions and balance", Flags: flag.NewFlagSet("transactions-cc", flag.ExitOnError), CheckFlags: checkServerFlags, Do: ccTransactions, } func init() { defineServerFlags(ccTransactionsCommand.Flags) ccTransactionsCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") } func ccTransactions() { client, query := newRequest() uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.CCStatementRequest{ TrnUID: *uid, CCAcctFrom: ofxgo.CCAcct{ AcctID: ofxgo.String(acctID), }, Include: true, } query.CreditCard = append(query.CreditCard, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.Request(query) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } 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.CreditCard) < 1 { fmt.Println("No banking messages received") return } if stmt, ok := response.CreditCard[0].(*ofxgo.CCStatementResponse); ok { fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf) fmt.Println("Transactions:") for _, tran := range stmt.BankTranList.Transactions { currency := stmt.CurDef if tran.Currency != nil { currency = tran.Currency.CurSym } var name string if len(tran.Name) > 0 { name = string(tran.Name) } else { name = string(tran.Payee.Name) } if len(tran.Memo) > 0 { name = name + " - " + string(tran.Memo) } fmt.Printf("%s %-15s %-11s %s\n", tran.DtPosted, tran.TrnAmt.String()+" "+currency.String(), tran.TrnType, name) } } } ================================================ FILE: cmd/ofx/command.go ================================================ package main import ( "flag" "fmt" "golang.org/x/term" "os" ) type command struct { Name string Description string Flags *flag.FlagSet CheckFlags func() bool // Check the flag values after they're parsed, printing errors and returning false if they're incorrect Do func() // Run the command (only called if CheckFlags returns true) } func (c *command) usage() { fmt.Printf("Usage of %s:\n", c.Name) c.Flags.PrintDefaults() } // flags common to all server transactions var serverURL, username, password, org, fid, appID, appVer, ofxVersion, clientUID string var noIndentRequests bool var carriageReturn bool var dryrun bool var userAgent string func defineServerFlags(f *flag.FlagSet) { f.StringVar(&serverURL, "url", "", "Financial institution's OFX Server URL (see ofxhome.com if you don't know it)") f.StringVar(&username, "username", "", "Your username at financial institution") f.StringVar(&password, "password", "", "Your password at financial institution") f.StringVar(&org, "org", "", "'ORG' for your financial institution") f.StringVar(&fid, "fid", "", "'FID' for your financial institution") f.StringVar(&appID, "appid", "QWIN", "'APPID' to pretend to be") f.StringVar(&appVer, "appver", "2400", "'APPVER' to pretend to be") f.StringVar(&ofxVersion, "ofxversion", "203", "OFX version to use") f.StringVar(&clientUID, "clientuid", "", "Client UID (only required by a few FIs, like Chase)") f.BoolVar(&noIndentRequests, "noindent", false, "Don't indent OFX requests") 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 { var ret bool = true if len(serverURL) == 0 { fmt.Println("Error: Server URL empty") ret = false } if len(username) == 0 { fmt.Println("Error: Username empty") ret = false } if ret && len(password) == 0 { fmt.Printf("Password for %s: ", username) pass, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { fmt.Printf("Error reading password: %s\n", err) ret = false } else { password = string(pass) } } return ret } ================================================ FILE: cmd/ofx/detect_settings.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "os" "time" ) var detectSettingsCommand = command{ Name: "detect-settings", Description: "Attempt to guess client settings needed for a particular financial institution", Flags: flag.NewFlagSet("detect-settings", flag.ExitOnError), CheckFlags: checkServerFlags, Do: detectSettings, } var delay uint64 func init() { detectSettingsCommand.Flags.StringVar(&serverURL, "url", "", "Financial institution's OFX Server URL (see ofxhome.com if you don't know it)") detectSettingsCommand.Flags.StringVar(&username, "username", "", "Your username at financial institution") detectSettingsCommand.Flags.StringVar(&password, "password", "", "Your password at financial institution") detectSettingsCommand.Flags.StringVar(&org, "org", "", "'ORG' for your financial institution") detectSettingsCommand.Flags.StringVar(&fid, "fid", "", "'FID' for your financial institution") detectSettingsCommand.Flags.Uint64Var(&delay, "delay", 500, "How long to delay between two subsequent requests, in milliseconds") } // We keep a separate list of APPIDs to preserve the ordering (ordering isn't // guaranteed in maps). We want to try them in order from 'best' and most // likely to work to 'worse' and least likely to work var appIDs = []string{ "OFXGO", // ofxgo (this library) "QWIN", // Intuit Quicken Windows "QMOFX", // Intuit Quicken Mac "QB", // Intuit QuickBooks Windows "Money", // Microsoft Money 2007 } var appVersions = map[string][]string{ "OFXGO": { // ofxgo (this library) "0001", }, "QWIN": { // Intuit Quicken Windows "2600", // 2017 "2500", // 2016 "2400", // 2015 "2300", // 2014 "2200", // 2013 "2100", // 2012 "2000", // 2011 "1900", // 2010 "1800", // 2009 "1700", // 2008 "1600", // 2007 "1500", // 2006 "1400", // 2005 }, "QMOFX": { // Intuit Quicken Mac "1700", // 2008 "1600", // 2007 "1500", // 2006 "1400", // 2005 }, "QB": { // Intuit QuickBooks Windows "1800", // 2008 "1700", // 2007 "1600", // 2006 "1500", // 2005 }, "Money": { // Microsoft Money 2007 "1600", // 2007 "1500", // 2006 "1400", // 2005 "1200", // 2004 "1100", // 2003 }, } var versions = []string{ "203", "103", "200", "201", "202", "210", "211", "102", "151", "160", "220", } func detectSettings() { var attempts uint for _, appID := range appIDs { for _, appVer := range appVersions[appID] { for _, version := range versions { for _, noIndent := range []bool{false, true} { if tryProfile(appID, appVer, version, noIndent) { fmt.Println("The following settings were found to work:") fmt.Printf("AppID: %s\n", appID) fmt.Printf("AppVer: %s\n", appVer) fmt.Printf("OFX Version: %s\n", version) fmt.Printf("noindent: %t\n", noIndent) os.Exit(0) } else { attempts++ var noIndentString string if noIndent { noIndentString = " noindent" } fmt.Printf("Attempt %d failed (%s %s %s%s), trying again after %dms...\n", attempts, appID, appVer, version, noIndentString, delay) time.Sleep(time.Duration(delay) * time.Millisecond) } } } } } } const anonymous = "anonymous00000000000000000000000" func tryProfile(appID, appVer, version string, noindent bool) bool { ver, err := ofxgo.NewOfxVersion(version) if err != nil { fmt.Println("Error creating new OfxVersion enum:", err) os.Exit(1) } var client = ofxgo.GetClient(serverURL, &ofxgo.BasicClient{ AppID: appID, AppVer: appVer, SpecVersion: ver, NoIndent: noindent, }) var query ofxgo.Request query.URL = serverURL query.Signon.ClientUID = ofxgo.UID(clientUID) query.Signon.UserID = ofxgo.String(username) query.Signon.UserPass = ofxgo.String(password) query.Signon.Org = ofxgo.String(org) query.Signon.Fid = ofxgo.String(fid) uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } profileRequest := ofxgo.ProfileRequest{ TrnUID: *uid, DtProfUp: ofxgo.Date{Time: time.Unix(0, 0)}, } query.Prof = append(query.Prof, &profileRequest) _, err = client.Request(&query) if err == nil { return true } // try again with anonymous logins query.Signon.UserID = ofxgo.String(anonymous) query.Signon.UserPass = ofxgo.String(anonymous) _, err = client.Request(&query) return err == nil } ================================================ FILE: cmd/ofx/get_accounts.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "os" "time" ) var getAccountsCommand = command{ Name: "get-accounts", Description: "List accounts at your financial institution", Flags: flag.NewFlagSet("get-accounts", flag.ExitOnError), CheckFlags: checkServerFlags, Do: getAccounts, } func init() { defineServerFlags(getAccountsCommand.Flags) } func getAccounts() { client, query := newRequest() uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } acctInfo := ofxgo.AcctInfoRequest{ TrnUID: *uid, DtAcctUp: ofxgo.Date{Time: time.Unix(0, 0)}, } query.Signup = append(query.Signup, &acctInfo) if dryrun { printRequest(client, query) return } response, err := client.Request(query) if err != nil { fmt.Println("Error requesting account information:", err) os.Exit(1) } 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.Signup) < 1 { fmt.Println("No signup messages received") return } fmt.Printf("\nFound the following accounts:\n\n") if acctinfo, ok := response.Signup[0].(*ofxgo.AcctInfoResponse); ok { for _, acct := range acctinfo.AcctInfo { if acct.BankAcctInfo != nil { fmt.Printf("Bank Account:\n\tBankID: \"%s\"\n\tAcctID: \"%s\"\n\tAcctType: %s\n", acct.BankAcctInfo.BankAcctFrom.BankID, acct.BankAcctInfo.BankAcctFrom.AcctID, acct.BankAcctInfo.BankAcctFrom.AcctType) } else if acct.CCAcctInfo != nil { fmt.Printf("Credit card:\n\tAcctID: \"%s\"\n", acct.CCAcctInfo.CCAcctFrom.AcctID) } else if acct.InvAcctInfo != nil { fmt.Printf("Investment account:\n\tBrokerID: \"%s\"\n\tAcctID: \"%s\"\n", acct.InvAcctInfo.InvAcctFrom.BrokerID, acct.InvAcctInfo.InvAcctFrom.AcctID) } else { fmt.Printf("Unknown type: %s %s\n", acct.Name, acct.Desc) } } } } ================================================ FILE: cmd/ofx/invdownload.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "io" "os" ) var invDownloadCommand = command{ Name: "download-inv", Description: "Download a investment account statement to a file", Flags: flag.NewFlagSet("download-inv", flag.ExitOnError), CheckFlags: invDownloadCheckFlags, Do: invDownload, } var brokerID string func init() { defineServerFlags(invDownloadCommand.Flags) invDownloadCommand.Flags.StringVar(&filename, "filename", "./response.ofx", "The file to save to") invDownloadCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") invDownloadCommand.Flags.StringVar(&brokerID, "brokerid", "", "BrokerID (from `get-accounts` subcommand)") } func invDownloadCheckFlags() bool { ret := checkServerFlags() if len(filename) == 0 { fmt.Println("Error: Filename empty") return false } return ret } func invDownload() { client, query := newRequest() uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.InvStatementRequest{ TrnUID: *uid, InvAcctFrom: ofxgo.InvAcct{ BrokerID: ofxgo.String(brokerID), AcctID: ofxgo.String(acctID), }, Include: true, IncludeOO: true, IncludePos: true, IncludeBalance: true, Include401K: true, Include401KBal: true, } query.InvStmt = append(query.InvStmt, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.RequestNoParse(query) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } defer response.Body.Close() file, err := os.Create(filename) if err != nil { fmt.Println("Error creating file to write to:", err) os.Exit(1) } defer file.Close() _, err = io.Copy(file, response.Body) if err != nil { fmt.Println("Error writing response to file:", err) os.Exit(1) } } ================================================ FILE: cmd/ofx/invtransactions.go ================================================ package main import ( "flag" "fmt" "github.com/aclindsa/ofxgo" "os" ) var invTransactionsCommand = command{ Name: "transactions-inv", Description: "Print investment transactions", Flags: flag.NewFlagSet("transactions-inv", flag.ExitOnError), CheckFlags: checkServerFlags, Do: invTransactions, } func init() { defineServerFlags(invTransactionsCommand.Flags) invTransactionsCommand.Flags.StringVar(&acctID, "acctid", "", "AcctID (from `get-accounts` subcommand)") invTransactionsCommand.Flags.StringVar(&brokerID, "brokerid", "", "BrokerID (from `get-accounts` subcommand)") } func invTransactions() { client, query := newRequest() uid, err := ofxgo.RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := ofxgo.InvStatementRequest{ TrnUID: *uid, InvAcctFrom: ofxgo.InvAcct{ BrokerID: ofxgo.String(brokerID), AcctID: ofxgo.String(acctID), }, Include: true, IncludeOO: true, IncludePos: true, IncludeBalance: true, Include401K: true, Include401KBal: true, } query.InvStmt = append(query.InvStmt, &statementRequest) if dryrun { printRequest(client, query) return } response, err := client.Request(query) if err != nil { os.Exit(1) } 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.InvStmt) < 1 { fmt.Println("No investment messages received") return } if stmt, ok := response.InvStmt[0].(*ofxgo.InvStatementResponse); ok { availCash := stmt.InvBal.AvailCash if availCash.IsInt() && availCash.Num().Int64() != 0 { fmt.Printf("Balance: %s %s (as of %s)\n", stmt.InvBal.AvailCash, stmt.CurDef, stmt.DtAsOf) } for _, banktrans := range stmt.InvTranList.BankTransactions { fmt.Printf("\nBank Transactions for %s subaccount:\n", banktrans.SubAcctFund) for _, tran := range banktrans.Transactions { printTransaction(stmt.CurDef, &tran) } } fmt.Printf("\nInvestment Transactions:\n") for _, t := range stmt.InvTranList.InvTransactions { fmt.Printf("%-14s", t.TransactionType()) switch tran := t.(type) { case ofxgo.BuyDebt: printInvBuy(stmt.CurDef, &tran.InvBuy) case ofxgo.BuyMF: printInvBuy(stmt.CurDef, &tran.InvBuy) case ofxgo.BuyOpt: printInvBuy(stmt.CurDef, &tran.InvBuy) case ofxgo.BuyOther: printInvBuy(stmt.CurDef, &tran.InvBuy) case ofxgo.BuyStock: printInvBuy(stmt.CurDef, &tran.InvBuy) case ofxgo.ClosureOpt: printInvTran(&tran.InvTran) fmt.Printf("%s %s contracts (%d shares each)\n", tran.OptAction, tran.Units, tran.ShPerCtrct) case ofxgo.Income: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %s %s %s (%s %s)\n", tran.IncomeType, tran.Total, currency, tran.SecID.UniqueIDType, tran.SecID.UniqueID) // TODO print ticker instead of CUSIP case ofxgo.InvExpense: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %s %s (%s %s)\n", tran.Total, currency, tran.SecID.UniqueIDType, tran.SecID.UniqueID) // TODO print ticker instead of CUSIP case ofxgo.JrnlFund: printInvTran(&tran.InvTran) fmt.Printf(" %s %s (%s -> %s)\n", tran.Total, stmt.CurDef, tran.SubAcctFrom, tran.SubAcctTo) case ofxgo.JrnlSec: printInvTran(&tran.InvTran) fmt.Printf(" %s %s %s (%s -> %s)\n", tran.Units, tran.SecID.UniqueIDType, tran.SecID.UniqueID, tran.SubAcctFrom, tran.SubAcctTo) // TODO print ticker instead of CUSIP case ofxgo.MarginInterest: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %s %s\n", tran.Total, currency) case ofxgo.Reinvest: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %s (%s %s)@%s %s (Total: %s)\n", tran.Units, tran.SecID.UniqueIDType, tran.SecID.UniqueID, tran.UnitPrice, currency, tran.Total) // TODO print ticker instead of CUSIP case ofxgo.RetOfCap: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %s %s (%s %s)\n", tran.Total, currency, tran.SecID.UniqueIDType, tran.SecID.UniqueID) // TODO print ticker instead of CUSIP case ofxgo.SellDebt: printInvSell(stmt.CurDef, &tran.InvSell) case ofxgo.SellMF: printInvSell(stmt.CurDef, &tran.InvSell) case ofxgo.SellOpt: printInvSell(stmt.CurDef, &tran.InvSell) case ofxgo.SellOther: printInvSell(stmt.CurDef, &tran.InvSell) case ofxgo.SellStock: printInvSell(stmt.CurDef, &tran.InvSell) case ofxgo.Split: printInvTran(&tran.InvTran) currency := stmt.CurDef if ok, _ := tran.Currency.Valid(); ok { currency = tran.Currency.CurSym } fmt.Printf(" %d/%d %s -> %s shares of %s %s (%s %s for fractional shares)\n", tran.Numerator, tran.Denominator, tran.OldUnits, tran.NewUnits, tran.SecID.UniqueIDType, tran.SecID.UniqueID, tran.FracCash, currency) // TODO print ticker instead of CUSIP case ofxgo.Transfer: printInvTran(&tran.InvTran) fmt.Printf(" %s (%s %s) %s\n", tran.Units, tran.SecID.UniqueIDType, tran.SecID.UniqueID, tran.TferAction) // TODO print ticker instead of CUSIP } } } } func printInvTran(it *ofxgo.InvTran) { fmt.Printf("%s", it.DtTrade) } func printInvBuy(defCurrency ofxgo.CurrSymbol, ib *ofxgo.InvBuy) { printInvTran(&ib.InvTran) currency := defCurrency if ok, _ := ib.Currency.Valid(); ok { currency = ib.Currency.CurSym } fmt.Printf("%s (%s %s)@%s %s (Total: %s)\n", ib.Units, ib.SecID.UniqueIDType, ib.SecID.UniqueID, ib.UnitPrice, currency, ib.Total) // TODO print ticker instead of CUSIP } func printInvSell(defCurrency ofxgo.CurrSymbol, is *ofxgo.InvSell) { printInvTran(&is.InvTran) currency := defCurrency if ok, _ := is.Currency.Valid(); ok { currency = is.Currency.CurSym } fmt.Printf(" %s (%s %s)@%s %s (Total: %s)\n", is.Units, is.SecID.UniqueIDType, is.SecID.UniqueID, is.UnitPrice, currency.String(), is.Total) // TODO print ticker instead of CUSIP } ================================================ FILE: cmd/ofx/main.go ================================================ package main import ( "fmt" "os" "strconv" ) var commands = []command{ profileDownloadCommand, getAccountsCommand, downloadCommand, ccDownloadCommand, invDownloadCommand, bankTransactionsCommand, ccTransactionsCommand, invTransactionsCommand, detectSettingsCommand, } func usage() { fmt.Println(`The ofxgo command-line client provides a simple interface to query, parse, and display financial data via the OFX specification. Usage: ofx command [arguments] The commands are:`) maxlen := 0 for _, cmd := range commands { if len(cmd.Name) > maxlen { maxlen = len(cmd.Name) } } formatString := " %-" + strconv.Itoa(maxlen) + "s %s\n" for _, cmd := range commands { fmt.Printf(formatString, cmd.Name, cmd.Description) } } func runCmd(c *command) { err := c.Flags.Parse(os.Args[2:]) if err != nil { fmt.Printf("Error parsing flags: %s\n", err) c.usage() os.Exit(1) } if !c.CheckFlags() { fmt.Println() c.usage() os.Exit(1) } c.Do() } func main() { if len(os.Args) < 2 { fmt.Printf("Error: Please supply a sub-command. Usage:\n\n") usage() os.Exit(1) } cmdName := os.Args[1] for _, cmd := range commands { if cmd.Name == cmdName { runCmd(&cmd) os.Exit(0) } } switch cmdName { case "-h", "-help", "--help", "help": usage() default: fmt.Println("Error: Invalid sub-command. Usage:") usage() os.Exit(1) } } ================================================ FILE: cmd/ofx/profiledownload.go ================================================ 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) } } ================================================ FILE: cmd/ofx/util.go ================================================ package main import ( "fmt" "github.com/aclindsa/ofxgo" "os" ) func newRequest() (ofxgo.Client, *ofxgo.Request) { ver, err := ofxgo.NewOfxVersion(ofxVersion) if err != nil { fmt.Println("Error creating new OfxVersion enum:", err) os.Exit(1) } var client = ofxgo.GetClient(serverURL, &ofxgo.BasicClient{ AppID: appID, AppVer: appVer, SpecVersion: ver, NoIndent: noIndentRequests, CarriageReturn: carriageReturn, UserAgent: userAgent, }) var query ofxgo.Request query.URL = serverURL query.Signon.ClientUID = ofxgo.UID(clientUID) query.Signon.UserID = ofxgo.String(username) query.Signon.UserPass = ofxgo.String(password) query.Signon.Org = ofxgo.String(org) query.Signon.Fid = ofxgo.String(fid) return client, &query } 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) } ================================================ FILE: common.go ================================================ package ofxgo //go:generate ./generate_constants.py import ( "bytes" "errors" "fmt" "strings" "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(``) if carriageReturn { b.WriteByte('\r') } b.WriteByte('\n') b.WriteString(``) 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 // marshalling and unmarshalling. type Message interface { Name() string // The name of the OFX transaction wrapper element this represents Valid(version ofxVersion) (bool, error) // Called before a Message is marshaled and after it's unmarshaled to ensure the request or response is valid Type() messageType // The message set this message belongs to } type messageType uint // These constants are returned by Messages' Type() functions to determine // which message set they belong to const ( // Requests SignonRq messageType = iota SignupRq BankRq CreditCardRq LoanRq InvStmtRq InterXferRq WireXferRq BillpayRq EmailRq SecListRq PresDirRq PresDlvRq ProfRq ImageRq // Responses SignonRs SignupRs BankRs CreditCardRs LoanRs InvStmtRs InterXferRs WireXferRs BillpayRs EmailRs SecListRs PresDirRs PresDlvRs ProfRs ImageRs ) func (t messageType) String() string { switch t { case SignonRq: return "SIGNONMSGSRQV1" case SignupRq: return "SIGNUPMSGSRQV1" case BankRq: return "BANKMSGSRQV1" case CreditCardRq: return "CREDITCARDMSGSRQV1" case LoanRq: return "LOANMSGSRQV1" case InvStmtRq: return "INVSTMTMSGSRQV1" case InterXferRq: return "INTERXFERMSGSRQV1" case WireXferRq: return "WIREXFERMSGSRQV1" case BillpayRq: return "BILLPAYMSGSRQV1" case EmailRq: return "EMAILMSGSRQV1" case SecListRq: return "SECLISTMSGSRQV1" case PresDirRq: return "PRESDIRMSGSRQV1" case PresDlvRq: return "PRESDLVMSGSRQV1" case ProfRq: return "PROFMSGSRQV1" case ImageRq: return "IMAGEMSGSRQV1" case SignonRs: return "SIGNONMSGSRSV1" case SignupRs: return "SIGNUPMSGSRSV1" case BankRs: return "BANKMSGSRSV1" case CreditCardRs: return "CREDITCARDMSGSRSV1" case LoanRs: return "LOANMSGSRSV1" case InvStmtRs: return "INVSTMTMSGSRSV1" case InterXferRs: return "INTERXFERMSGSRSV1" case WireXferRs: return "WIREXFERMSGSRSV1" case BillpayRs: return "BILLPAYMSGSRSV1" case EmailRs: return "EMAILMSGSRSV1" case SecListRs: return "SECLISTMSGSRSV1" case PresDirRs: return "PRESDIRMSGSRSV1" case PresDlvRs: return "PRESDLVMSGSRSV1" case ProfRs: return "PROFMSGSRSV1" case ImageRs: return "IMAGEMSGSRSV1" } panic("Invalid messageType") } // Map of error codes to their meanings, SEVERITY, and conditions under which // OFX servers are expected to return them var statusMeanings = map[Int][3]string{ 0: {"Success", "INFO", "The server successfully processed the request."}, 1: {"Client is up-to-date", "INFO", "Based on the client timestamp, the client has the latest information. The response does not supply any additional information."}, 2000: {"General error", "ERROR", "Error other than those specified by the remaining error codes. Note: Servers should provide a more specific error whenever possible. Error code 2000 should be reserved for cases in which a more specific code is not available."}, 2001: {"Invalid account", "ERROR", ""}, 2002: {"General account error", "ERROR", "Account error not specified by the remaining error codes."}, 2003: {"Account not found", "ERROR", "The specified account number does not correspond to one of the user’s accounts."}, 2004: {"Account closed", "ERROR", "The specified account number corresponds to an account that has been closed."}, 2005: {"Account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."}, 2006: {"Source account not found", "ERROR", "The specified account number does not correspond to one of the user’s accounts."}, 2007: {"Source account closed", "ERROR", "The specified account number corresponds to an account that has been closed."}, 2008: {"Source account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."}, 2009: {"Destination account not found", "ERROR", "The specified account number does not correspond to one of the user’s accounts."}, 2010: {"Destination account closed", "ERROR", "The specified account number corresponds to an account that has been closed."}, 2011: {"Destination account not authorized", "ERROR", "The user is not authorized to perform this action on the account, or the server does not allow this type of action to be performed on the account."}, 2012: {"Invalid amount", "ERROR", "The specified amount is not valid for this action; for example, the user specified a negative payment amount."}, 2014: {"Date too soon", "ERROR", "The server cannot process the requested action by the date specified by the user."}, 2015: {"Date too far in future", "ERROR", "The server cannot accept requests for an action that far in the future."}, 2016: {"Transaction already committed", "ERROR", "Transaction has entered the processing loop and cannot be modified/cancelled using OFX. The transaction may still be cancelled or modified using other means (for example, a phone call to Customer Service)."}, 2017: {"Already canceled", "ERROR", "The transaction cannot be canceled or modified because it has already been canceled."}, 2018: {"Unknown server ID", "ERROR", "The specified server ID does not exist or no longer exists."}, 2019: {"Duplicate request", "ERROR", "A request with this has already been received and processed."}, 2020: {"Invalid date", "ERROR", "The specified datetime stamp cannot be parsed; for instance, the datetime stamp specifies 25:00 hours."}, 2021: {"Unsupported version", "ERROR", "The server does not support the requested version. The version of the message set specified by the client is not supported by this server."}, 2022: {"Invalid TAN", "ERROR", "The server was unable to validate the TAN sent in the request."}, 2023: {"Unknown FITID", "ERROR", "The specified FITID/BILLID does not exist or no longer exists. [BILLID not found (ERROR) in the billing message sets]"}, 2025: {"Branch ID missing", "ERROR", "A value must be provided in the aggregate for this country system, but this field is missing."}, 2026: {"Bank name doesn’t match bank ID", "ERROR", "The value of in the aggregate is inconsistent with the value of in the aggregate."}, 2027: {"Invalid date range", "ERROR", "Response for non-overlapping dates, date ranges in the future, et cetera."}, 2028: {"Requested element unknown", "WARN", "One or more elements of the request were not recognized by the server or the server (as noted in the FI Profile) does not support the elements. The server executed the element transactions it understood and supported. For example, the request file included private tags in a but the server was able to execute the rest of the request."}, 3000: {"MFA Challenge authentication required", "ERROR", "User credentials are correct, but further authentication required. Client should send in next request."}, 3001: {"MFA Challenge information is invalid", "ERROR", "User or client information sent in MFACHALLENGEA contains invalid information"}, 6500: {"Y invalid without ", "ERROR", "This error code may appear in the element of an wrapper (in and V2 message set responses) or the contained in any embedded transaction wrappers within a sync response. The corresponding sync request wrapper included Y with Y or Y, which is illegal."}, 6501: {"Embedded transactions in request failed to process: Out of date", "WARN", "Y and embedded transactions appeared in the request sync wrapper and the provided was out of date. This code should be used in the of the response sync wrapper."}, 6502: {"Unable to process embedded transaction due to out-of-date ", "ERROR", "Used in response transaction wrapper for embedded transactions when 6501 appears in the surrounding sync wrapper."}, 10000: {"Stop check in process", "INFO", "Stop check is already in process."}, 10500: {"Too many checks to process", "ERROR", "The stop-payment request specifies too many checks."}, 10501: {"Invalid payee", "ERROR", "Payee error not specified by the remaining error codes."}, 10502: {"Invalid payee address", "ERROR", "Some portion of the payee’s address is incorrect or unknown."}, 10503: {"Invalid payee account number", "ERROR", "The account number of the requested payee is invalid."}, 10504: {"Insufficient funds", "ERROR", "The server cannot process the request because the specified account does not have enough funds."}, 10505: {"Cannot modify element", "ERROR", "The server does not allow modifications to one or more values in a modification request."}, 10506: {"Cannot modify source account", "ERROR", "Reserved for future use."}, 10507: {"Cannot modify destination account", "ERROR", "Reserved for future use."}, 10508: {"Invalid frequency", "ERROR", "The specified frequency does not match one of the accepted frequencies for recurring transactions."}, 10509: {"Model already canceled", "ERROR", "The server has already canceled the specified recurring model."}, 10510: {"Invalid payee ID", "ERROR", "The specified payee ID does not exist or no longer exists."}, 10511: {"Invalid payee city", "ERROR", "The specified city is incorrect or unknown."}, 10512: {"Invalid payee state", "ERROR", "The specified state is incorrect or unknown."}, 10513: {"Invalid payee postal code", "ERROR", "The specified postal code is incorrect or unknown."}, 10514: {"Transaction already processed", "ERROR", "Transaction has already been sent or date due is past"}, 10515: {"Payee not modifiable by client", "ERROR", "The server does not allow clients to change payee information."}, 10516: {"Wire beneficiary invalid", "ERROR", "The specified wire beneficiary does not exist or no longer exists."}, 10517: {"Invalid payee name", "ERROR", "The server does not recognize the specified payee name."}, 10518: {"Unknown model ID", "ERROR", "The specified model ID does not exist or no longer exists."}, 10519: {"Invalid payee list ID", "ERROR", "The specified payee list ID does not exist or no longer exists."}, 10600: {"Table type not found", "ERROR", "The specified table type is not recognized or does not exist."}, 12250: {"Investment transaction download not supported", "WARN", "The server does not support investment transaction download."}, 12251: {"Investment position download not supported", "WARN", "The server does not support investment position download."}, 12252: {"Investment positions for specified date not available", "WARN", "The server does not support investment positions for the specified date."}, 12253: {"Investment open order download not supported", "WARN", "The server does not support open order download."}, 12254: {"Investment balances download not supported", "WARN", "The server does not support investment balances download."}, 12255: {"401(k) not available for this account", "ERROR", "401(k) information requested from a non- 401(k) account."}, 12500: {"One or more securities not found", "ERROR", "The server could not find the requested securities."}, 13000: {"User ID & password will be sent out-of-band", "INFO", "The server will send the user ID and password via postal mail, e-mail, or another means. The accompanying message will provide details."}, 13500: {"Unable to enroll user", "ERROR", "The server could not enroll the user."}, 13501: {"User already enrolled", "ERROR", "The server has already enrolled the user."}, 13502: {"Invalid service", "ERROR", "The server does not support the service specified in the service-activation request."}, 13503: {"Cannot change user information", "ERROR", "The server does not support the request."}, 13504: {" Missing or Invalid in ", "ERROR", "The FI requires the client to provide the aggregate in the request, but either none was provided, or the one provided was invalid."}, 14500: {"1099 forms not available", "ERROR", "1099 forms are not yet available for the tax year requested."}, 14501: {"1099 forms not available for user ID", "ERROR", "This user does not have any 1099 forms available."}, 14600: {"W2 forms not available", "ERROR", "W2 forms are not yet available for the tax year requested."}, 14601: {"W2 forms not available for user ID", "ERROR", "The user does not have any W2 forms available."}, 14700: {"1098 forms not available", "ERROR", "1098 forms are not yet available for the tax year requested."}, 14701: {"1098 forms not available for user ID", "ERROR", "The user does not have any 1098 forms available."}, 15000: {"Must change USERPASS", "INFO", "The user must change his or her number as part of the next OFX request."}, 15500: {"Signon invalid", "ERROR", "The user cannot signon because he or she entered an invalid user ID or password."}, 15501: {"Customer account already in use", "ERROR", "The server allows only one connection at a time, and another user is already signed on. Please try again later."}, 15502: {"USERPASS lockout", "ERROR", "The server has received too many failed signon attempts for this user. Please call the FI’s technical support number."}, 15503: {"Could not change USERPASS", "ERROR", "The server does not support the request."}, 15504: {"Could not provide random data", "ERROR", "The server could not generate random data as requested by the ."}, 15505: {"Country system not supported", "ERROR", "The server does not support the country specified in the field of the aggregate."}, 15506: {"Empty signon not supported", "ERROR", "The server does not support signons not accompanied by some other transaction."}, 15507: {"Signon invalid without supporting pin change request", "ERROR", "The OFX block associated with the signon does not contain a pin change request and should."}, 15508: {"Transaction not authorized. ", "ERROR", "Current user is not authorized to perform this action on behalf of the ."}, 15510: {"CLIENTUID error", "ERROR", "The CLIENTUID sent by the client was incorrect. User must register the Client UID."}, 15511: {"MFA error", "ERROR", "User should contact financial institution."}, 15512: {"AUTHTOKEN required", "ERROR", "User needs to contact financial institution to obtain AUTHTOKEN. Client should send it in the next request."}, 15513: {"AUTHTOKEN invalid", "ERROR", "The AUTHTOKEN sent by the client was invalid."}, 16500: {"HTML not allowed", "ERROR", "The server does not accept HTML formatting in the request."}, 16501: {"Unknown mail To:", "ERROR", "The server was unable to send mail to the specified Internet address."}, 16502: {"Invalid URL", "ERROR", "The server could not parse the URL."}, 16503: {"Unable to get URL", "ERROR", "The server was unable to retrieve the information at this URL (e.g., an HTTP 400 or 500 series error)."}, } // Status represents the status of a Response (both top-level Request objects, // and *Response objects) type Status struct { XMLName xml.Name `xml:"STATUS"` Code Int `xml:"CODE"` Severity String `xml:"SEVERITY"` Message String `xml:"MESSAGE,omitempty"` } // Valid returns whether the Status is valid according to the OFX spec func (s *Status) Valid() (bool, error) { switch s.Severity { case "INFO", "WARN", "ERROR": default: return false, errors.New("Invalid STATUS>SEVERITY") } if arr, ok := statusMeanings[s.Code]; ok { if arr[1] != string(s.Severity) { return false, errors.New("Unexpected SEVERITY for STATUS>CODE") } } else { return false, errors.New("Unknown OFX status code") } return true, nil } // CodeMeaning returns the meaning of the current status Code func (s *Status) CodeMeaning() (string, error) { if arr, ok := statusMeanings[s.Code]; ok { return arr[0], nil } return "", errors.New("Unknown OFX status code") } // CodeConditions returns the conditions under which an OFX server is expected // to return the current status Code func (s *Status) CodeConditions() (string, error) { if arr, ok := statusMeanings[s.Code]; ok { return arr[2], nil } return "", errors.New("Unknown OFX status code") } // BankAcct represents the identifying information for one bank account type BankAcct struct { XMLName xml.Name // BANKACCTTO or BANKACCTFROM BankID String `xml:"BANKID"` BranchID String `xml:"BRANCHID,omitempty"` // Unused in USA AcctID String `xml:"ACCTID"` AcctType acctType `xml:"ACCTTYPE"` // One of CHECKING, SAVINGS, MONEYMRKT, CREDITLINE, CD AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA } // Valid returns whether the BankAcct is valid according to the OFX spec func (b BankAcct) Valid() (bool, error) { if len(b.BankID) == 0 { return false, errors.New("BankAcct.BankID empty") } if len(b.AcctID) == 0 { return false, errors.New("BankAcct.AcctID empty") } if !b.AcctType.Valid() { return false, errors.New("Invalid or unspecified BankAcct.AcctType") } return true, nil } // CCAcct represents the identifying information for one checking account type CCAcct struct { XMLName xml.Name // CCACCTTO or CCACCTFROM AcctID String `xml:"ACCTID"` AcctKey String `xml:"ACCTKEY,omitempty"` // Unused in USA } // Valid returns whether the CCAcct is valid according to the OFX spec func (c CCAcct) Valid() (bool, error) { if len(c.AcctID) == 0 { return false, errors.New("CCAcct.AcctID empty") } return true, nil } // InvAcct represents the identifying information for one investment account type InvAcct struct { XMLName xml.Name // INVACCTTO or INVACCTFROM BrokerID String `xml:"BROKERID"` AcctID String `xml:"ACCTID"` } // Currency represents one ISO-4217 currency. CURRENCY elements signify that // the transaction containing this Currency struct is in this currency instead // of being converted to the statement's default. ORIGCURRENCY elements signify // that the transaction containing this Currency struct was converted to the // statement's default from the specified currency. type Currency struct { XMLName xml.Name // CURRENCY or ORIGCURRENCY CurRate Amount `xml:"CURRATE"` // Ratio of statement's currency (CURDEF) to transaction currency (CURSYM) CurSym CurrSymbol `xml:"CURSYM"` // ISO-4217 3-character currency identifier } // Valid returns whether the Currency is valid according to the OFX spec func (c Currency) Valid() (bool, error) { if c.CurRate.IsInt() && c.CurRate.Num().Int64() == 0 { return false, errors.New("CurRate may not be zero") } else if ok, err := c.CurSym.Valid(); !ok { return false, err } return true, nil } ================================================ FILE: common_test.go ================================================ package ofxgo import ( "testing" ) func TestStatusValid(t *testing.T) { s := Status{ Code: 0, Severity: "INFO", Message: "Success", } if ok, err := s.Valid(); !ok { t.Fatalf("Status unexpectedly invalid: %s\n", err) } s.Severity = "INVALID" if ok, err := s.Valid(); ok || err == nil { t.Fatalf("Status unexpectedly valid invalid Severity\n") } s.Severity = "WARN" if ok, err := s.Valid(); ok || err == nil { t.Fatalf("Status unexpectedly valid with wrong Severity for Code 0\n") } s.Code = 9 if ok, err := s.Valid(); ok || err == nil { t.Fatalf("Status unexpectedly valid with invalid Code\n") } } func TestStatusCodeMeaning(t *testing.T) { s := Status{ Code: 15500, Severity: "ERROR", } meaning, err := s.CodeMeaning() if err != nil { t.Fatalf("Status.CodeMeaning unexpectedly failed: %s\n", err) } if meaning != "Signon invalid" { t.Fatalf("Unexpected meaning for Code 15500: \"%s\"\n", meaning) } s.Code = 999 if meaning, err := s.CodeMeaning(); len(meaning) != 0 || err == nil { t.Fatalf("Status.CodeMeaning unexpectedly succeeded with invalid Code\n") } } func TestStatusCodeConditions(t *testing.T) { s := Status{ Code: 2006, Severity: "ERROR", } if conditions, err := s.CodeConditions(); len(conditions) == 0 || err != nil { t.Fatalf("Status.CodeConditions unexpectedly failed: %s\n", err) } s.Code = 999 if conditions, err := s.CodeConditions(); len(conditions) != 0 || err == nil { t.Fatalf("Status.CodeConditions unexpectedly succeeded with invalid Code\n") } } ================================================ FILE: constants.go ================================================ package ofxgo /* * Do not edit this file by hand. It is auto-generated by calling `go generate`. * To make changes, edit generate_constants.py, re-run `go generate`, and check * in the result. */ import ( "errors" "fmt" "strings" "github.com/aclindsa/xml" ) type ofxVersion uint // OfxVersion* constants represent the OFX specification version in use const ( OfxVersion102 ofxVersion = 1 + iota OfxVersion103 OfxVersion151 OfxVersion160 OfxVersion200 OfxVersion201 OfxVersion202 OfxVersion203 OfxVersion210 OfxVersion211 OfxVersion220 ) var ofxVersions = [...]string{"102", "103", "151", "160", "200", "201", "202", "203", "210", "211", "220"} func (e ofxVersion) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OfxVersion102 && e <= OfxVersion220 } func (e ofxVersion) String() string { if e.Valid() { return ofxVersions[e-1] } return fmt.Sprintf("invalid ofxVersion (%d)", e) } func (e *ofxVersion) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range ofxVersions { if s == value { *e = ofxVersion(i + 1) return nil } } *e = 0 return errors.New("Invalid OfxVersion: \"" + in + "\"") } func (e *ofxVersion) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e ofxVersion) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(ofxVersions[e-1], start) return nil } // NewOfxVersion returns returns an 'enum' value of type ofxVersion given its // string representation func NewOfxVersion(s string) (ofxVersion, error) { var e ofxVersion err := e.FromString(s) if err != nil { return 0, err } return e, nil } type acctType uint // AcctType* constants represent types of bank accounts const ( AcctTypeChecking acctType = 1 + iota AcctTypeSavings AcctTypeMoneyMrkt AcctTypeCreditLine AcctTypeCD ) var acctTypes = [...]string{"CHECKING", "SAVINGS", "MONEYMRKT", "CREDITLINE", "CD"} func (e acctType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= AcctTypeChecking && e <= AcctTypeCD } func (e acctType) String() string { if e.Valid() { return acctTypes[e-1] } return fmt.Sprintf("invalid acctType (%d)", e) } func (e *acctType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range acctTypes { if s == value { *e = acctType(i + 1) return nil } } *e = 0 return errors.New("Invalid AcctType: \"" + in + "\"") } func (e *acctType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e acctType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(acctTypes[e-1], start) return nil } // NewAcctType returns returns an 'enum' value of type acctType given its // string representation func NewAcctType(s string) (acctType, error) { var e acctType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type trnType uint // TrnType* constants represent types of transactions. INT, ATM, and POS depend on the signage of the account. const ( TrnTypeCredit trnType = 1 + iota TrnTypeDebit TrnTypeInt TrnTypeDiv TrnTypeFee TrnTypeSrvChg TrnTypeDep TrnTypeATM TrnTypePOS TrnTypeXfer TrnTypeCheck TrnTypePayment TrnTypeCash TrnTypeDirectDep TrnTypeDirectDebit TrnTypeRepeatPmt TrnTypeHold TrnTypeOther ) var trnTypes = [...]string{"CREDIT", "DEBIT", "INT", "DIV", "FEE", "SRVCHG", "DEP", "ATM", "POS", "XFER", "CHECK", "PAYMENT", "CASH", "DIRECTDEP", "DIRECTDEBIT", "REPEATPMT", "HOLD", "OTHER"} func (e trnType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= TrnTypeCredit && e <= TrnTypeOther } func (e trnType) String() string { if e.Valid() { return trnTypes[e-1] } return fmt.Sprintf("invalid trnType (%d)", e) } func (e *trnType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range trnTypes { if s == value { *e = trnType(i + 1) return nil } } *e = 0 return errors.New("Invalid TrnType: \"" + in + "\"") } func (e *trnType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e trnType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(trnTypes[e-1], start) return nil } // NewTrnType returns returns an 'enum' value of type trnType given its // string representation func NewTrnType(s string) (trnType, error) { var e trnType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type imageType uint // ImageType* constants represent what this image contains const ( ImageTypeStatement imageType = 1 + iota ImageTypeTransaction ImageTypeTax ) var imageTypes = [...]string{"STATEMENT", "TRANSACTION", "TAX"} func (e imageType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= ImageTypeStatement && e <= ImageTypeTax } func (e imageType) String() string { if e.Valid() { return imageTypes[e-1] } return fmt.Sprintf("invalid imageType (%d)", e) } func (e *imageType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range imageTypes { if s == value { *e = imageType(i + 1) return nil } } *e = 0 return errors.New("Invalid ImageType: \"" + in + "\"") } func (e *imageType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e imageType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(imageTypes[e-1], start) return nil } // NewImageType returns returns an 'enum' value of type imageType given its // string representation func NewImageType(s string) (imageType, error) { var e imageType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type imageRefType uint // ImageRefType* constants represent the type of reference to the image const ( ImageRefTypeOpaque imageRefType = 1 + iota ImageRefTypeURL ImageRefTypeFormURL ) var imageRefTypes = [...]string{"OPAQUE", "URL", "FORMURL"} func (e imageRefType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= ImageRefTypeOpaque && e <= ImageRefTypeFormURL } func (e imageRefType) String() string { if e.Valid() { return imageRefTypes[e-1] } return fmt.Sprintf("invalid imageRefType (%d)", e) } func (e *imageRefType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range imageRefTypes { if s == value { *e = imageRefType(i + 1) return nil } } *e = 0 return errors.New("Invalid ImageRefType: \"" + in + "\"") } func (e *imageRefType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e imageRefType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(imageRefTypes[e-1], start) return nil } // NewImageRefType returns returns an 'enum' value of type imageRefType given its // string representation func NewImageRefType(s string) (imageRefType, error) { var e imageRefType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type checkSup uint // CheckSup* constants represent what portions of the check this image contains const ( CheckSupFrontOnly checkSup = 1 + iota CheckSupBackOnly CheckSupFrontAndBack ) var checkSups = [...]string{"FRONTONLY", "BACKONLY", "FRONTANDBACK"} func (e checkSup) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= CheckSupFrontOnly && e <= CheckSupFrontAndBack } func (e checkSup) String() string { if e.Valid() { return checkSups[e-1] } return fmt.Sprintf("invalid checkSup (%d)", e) } func (e *checkSup) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range checkSups { if s == value { *e = checkSup(i + 1) return nil } } *e = 0 return errors.New("Invalid CheckSup: \"" + in + "\"") } func (e *checkSup) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e checkSup) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(checkSups[e-1], start) return nil } // NewCheckSup returns returns an 'enum' value of type checkSup given its // string representation func NewCheckSup(s string) (checkSup, error) { var e checkSup err := e.FromString(s) if err != nil { return 0, err } return e, nil } type correctAction uint // CorrectAction* constants represent whether this transaction correction replaces or deletes the transaction matching its CORRECTFITID const ( CorrectActionDelete correctAction = 1 + iota CorrectActionReplace ) var correctActions = [...]string{"DELETE", "REPLACE"} func (e correctAction) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= CorrectActionDelete && e <= CorrectActionReplace } func (e correctAction) String() string { if e.Valid() { return correctActions[e-1] } return fmt.Sprintf("invalid correctAction (%d)", e) } func (e *correctAction) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range correctActions { if s == value { *e = correctAction(i + 1) return nil } } *e = 0 return errors.New("Invalid CorrectAction: \"" + in + "\"") } func (e *correctAction) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e correctAction) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(correctActions[e-1], start) return nil } // NewCorrectAction returns returns an 'enum' value of type correctAction given its // string representation func NewCorrectAction(s string) (correctAction, error) { var e correctAction err := e.FromString(s) if err != nil { return 0, err } return e, nil } type balType uint // BalType* constants represent how this BAL's VALUE field should be interpreted const ( BalTypeDollar balType = 1 + iota BalTypePercent BalTypeNumber ) var balTypes = [...]string{"DOLLAR", "PERCENT", "NUMBER"} func (e balType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= BalTypeDollar && e <= BalTypeNumber } func (e balType) String() string { if e.Valid() { return balTypes[e-1] } return fmt.Sprintf("invalid balType (%d)", e) } func (e *balType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range balTypes { if s == value { *e = balType(i + 1) return nil } } *e = 0 return errors.New("Invalid BalType: \"" + in + "\"") } func (e *balType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e balType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(balTypes[e-1], start) return nil } // NewBalType returns returns an 'enum' value of type balType given its // string representation func NewBalType(s string) (balType, error) { var e balType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type inv401kSource uint // Inv401kSource* constants represent the source of money used for this security in a 401(k) account. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST. const ( Inv401kSourcePreTax inv401kSource = 1 + iota Inv401kSourceAfterTax Inv401kSourceMatch Inv401kSourceProfitSharing Inv401kSourceRollover Inv401kSourceOtherVest Inv401kSourceOtherNonVest ) var inv401kSources = [...]string{"PRETAX", "AFTERTAX", "MATCH", "PROFITSHARING", "ROLLOVER", "OTHERVEST", "OTHERNONVEST"} func (e inv401kSource) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= Inv401kSourcePreTax && e <= Inv401kSourceOtherNonVest } func (e inv401kSource) String() string { if e.Valid() { return inv401kSources[e-1] } return fmt.Sprintf("invalid inv401kSource (%d)", e) } func (e *inv401kSource) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range inv401kSources { if s == value { *e = inv401kSource(i + 1) return nil } } *e = 0 return errors.New("Invalid Inv401kSource: \"" + in + "\"") } func (e *inv401kSource) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e inv401kSource) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(inv401kSources[e-1], start) return nil } // NewInv401kSource returns returns an 'enum' value of type inv401kSource given its // string representation func NewInv401kSource(s string) (inv401kSource, error) { var e inv401kSource err := e.FromString(s) if err != nil { return 0, err } return e, nil } type subAcctType uint // SubAcctType* constants represent the sub-account type for a source and/or destination of a transaction. Used in fields named SubAcctFrom, SubAcctTo, SubAcctSec, SubAcctFund, HeldInAcct. const ( SubAcctTypeCash subAcctType = 1 + iota SubAcctTypeMargin SubAcctTypeShort SubAcctTypeOther ) var subAcctTypes = [...]string{"CASH", "MARGIN", "SHORT", "OTHER"} func (e subAcctType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SubAcctTypeCash && e <= SubAcctTypeOther } func (e subAcctType) String() string { if e.Valid() { return subAcctTypes[e-1] } return fmt.Sprintf("invalid subAcctType (%d)", e) } func (e *subAcctType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range subAcctTypes { if s == value { *e = subAcctType(i + 1) return nil } } *e = 0 return errors.New("Invalid SubAcctType: \"" + in + "\"") } func (e *subAcctType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e subAcctType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(subAcctTypes[e-1], start) return nil } // NewSubAcctType returns returns an 'enum' value of type subAcctType given its // string representation func NewSubAcctType(s string) (subAcctType, error) { var e subAcctType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type buyType uint // BuyType* constants represent types of purchases const ( BuyTypeBuy buyType = 1 + iota BuyTypeBuyToCover ) var buyTypes = [...]string{"BUY", "BUYTOCOVER"} func (e buyType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= BuyTypeBuy && e <= BuyTypeBuyToCover } func (e buyType) String() string { if e.Valid() { return buyTypes[e-1] } return fmt.Sprintf("invalid buyType (%d)", e) } func (e *buyType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range buyTypes { if s == value { *e = buyType(i + 1) return nil } } *e = 0 return errors.New("Invalid BuyType: \"" + in + "\"") } func (e *buyType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e buyType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(buyTypes[e-1], start) return nil } // NewBuyType returns returns an 'enum' value of type buyType given its // string representation func NewBuyType(s string) (buyType, error) { var e buyType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type optAction uint // OptAction* constants represent types of actions for options const ( OptActionExercise optAction = 1 + iota OptActionAssign OptActionExpire ) var optActions = [...]string{"EXERCISE", "ASSIGN", "EXPIRE"} func (e optAction) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OptActionExercise && e <= OptActionExpire } func (e optAction) String() string { if e.Valid() { return optActions[e-1] } return fmt.Sprintf("invalid optAction (%d)", e) } func (e *optAction) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range optActions { if s == value { *e = optAction(i + 1) return nil } } *e = 0 return errors.New("Invalid OptAction: \"" + in + "\"") } func (e *optAction) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e optAction) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(optActions[e-1], start) return nil } // NewOptAction returns returns an 'enum' value of type optAction given its // string representation func NewOptAction(s string) (optAction, error) { var e optAction err := e.FromString(s) if err != nil { return 0, err } return e, nil } type tferAction uint // TferAction* constants represent whether the transfer is into or out of this account const ( TferActionIn tferAction = 1 + iota TferActionOut ) var tferActions = [...]string{"IN", "OUT"} func (e tferAction) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= TferActionIn && e <= TferActionOut } func (e tferAction) String() string { if e.Valid() { return tferActions[e-1] } return fmt.Sprintf("invalid tferAction (%d)", e) } func (e *tferAction) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range tferActions { if s == value { *e = tferAction(i + 1) return nil } } *e = 0 return errors.New("Invalid TferAction: \"" + in + "\"") } func (e *tferAction) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e tferAction) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(tferActions[e-1], start) return nil } // NewTferAction returns returns an 'enum' value of type tferAction given its // string representation func NewTferAction(s string) (tferAction, error) { var e tferAction err := e.FromString(s) if err != nil { return 0, err } return e, nil } type posType uint // PosType* constants represent position type const ( PosTypeLong posType = 1 + iota PosTypeShort ) var posTypes = [...]string{"LONG", "SHORT"} func (e posType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= PosTypeLong && e <= PosTypeShort } func (e posType) String() string { if e.Valid() { return posTypes[e-1] } return fmt.Sprintf("invalid posType (%d)", e) } func (e *posType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range posTypes { if s == value { *e = posType(i + 1) return nil } } *e = 0 return errors.New("Invalid PosType: \"" + in + "\"") } func (e *posType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e posType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(posTypes[e-1], start) return nil } // NewPosType returns returns an 'enum' value of type posType given its // string representation func NewPosType(s string) (posType, error) { var e posType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type secured uint // Secured* constants represent how an option is secured const ( SecuredNaked secured = 1 + iota SecuredCovered ) var secureds = [...]string{"NAKED", "COVERED"} func (e secured) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SecuredNaked && e <= SecuredCovered } func (e secured) String() string { if e.Valid() { return secureds[e-1] } return fmt.Sprintf("invalid secured (%d)", e) } func (e *secured) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range secureds { if s == value { *e = secured(i + 1) return nil } } *e = 0 return errors.New("Invalid Secured: \"" + in + "\"") } func (e *secured) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e secured) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(secureds[e-1], start) return nil } // NewSecured returns returns an 'enum' value of type secured given its // string representation func NewSecured(s string) (secured, error) { var e secured err := e.FromString(s) if err != nil { return 0, err } return e, nil } type duration uint // Duration* constants represent how long the investment order is good for const ( DurationDay duration = 1 + iota DurationGoodTilCancel DurationImmediate ) var durations = [...]string{"DAY", "GOODTILCANCEL", "IMMEDIATE"} func (e duration) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= DurationDay && e <= DurationImmediate } func (e duration) String() string { if e.Valid() { return durations[e-1] } return fmt.Sprintf("invalid duration (%d)", e) } func (e *duration) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range durations { if s == value { *e = duration(i + 1) return nil } } *e = 0 return errors.New("Invalid Duration: \"" + in + "\"") } func (e *duration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e duration) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(durations[e-1], start) return nil } // NewDuration returns returns an 'enum' value of type duration given its // string representation func NewDuration(s string) (duration, error) { var e duration err := e.FromString(s) if err != nil { return 0, err } return e, nil } type restriction uint // Restriction* constants represent a special restriction on an investment order const ( RestrictionAllOrNone restriction = 1 + iota RestrictionMinUnits RestrictionNone ) var restrictions = [...]string{"ALLORNONE", "MINUNITS", "NONE"} func (e restriction) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= RestrictionAllOrNone && e <= RestrictionNone } func (e restriction) String() string { if e.Valid() { return restrictions[e-1] } return fmt.Sprintf("invalid restriction (%d)", e) } func (e *restriction) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range restrictions { if s == value { *e = restriction(i + 1) return nil } } *e = 0 return errors.New("Invalid Restriction: \"" + in + "\"") } func (e *restriction) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e restriction) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(restrictions[e-1], start) return nil } // NewRestriction returns returns an 'enum' value of type restriction given its // string representation func NewRestriction(s string) (restriction, error) { var e restriction err := e.FromString(s) if err != nil { return 0, err } return e, nil } type unitType uint // UnitType* constants represent type of the UNITS value const ( UnitTypeShares unitType = 1 + iota UnitTypeCurrency ) var unitTypes = [...]string{"SHARES", "CURRENCY"} func (e unitType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= UnitTypeShares && e <= UnitTypeCurrency } func (e unitType) String() string { if e.Valid() { return unitTypes[e-1] } return fmt.Sprintf("invalid unitType (%d)", e) } func (e *unitType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range unitTypes { if s == value { *e = unitType(i + 1) return nil } } *e = 0 return errors.New("Invalid UnitType: \"" + in + "\"") } func (e *unitType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e unitType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(unitTypes[e-1], start) return nil } // NewUnitType returns returns an 'enum' value of type unitType given its // string representation func NewUnitType(s string) (unitType, error) { var e unitType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type optBuyType uint // OptBuyType* constants represent types of purchases for options const ( OptBuyTypeBuyToOpen optBuyType = 1 + iota OptBuyTypeBuyToClose ) var optBuyTypes = [...]string{"BUYTOOPEN", "BUYTOCLOSE"} func (e optBuyType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OptBuyTypeBuyToOpen && e <= OptBuyTypeBuyToClose } func (e optBuyType) String() string { if e.Valid() { return optBuyTypes[e-1] } return fmt.Sprintf("invalid optBuyType (%d)", e) } func (e *optBuyType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range optBuyTypes { if s == value { *e = optBuyType(i + 1) return nil } } *e = 0 return errors.New("Invalid OptBuyType: \"" + in + "\"") } func (e *optBuyType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e optBuyType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(optBuyTypes[e-1], start) return nil } // NewOptBuyType returns returns an 'enum' value of type optBuyType given its // string representation func NewOptBuyType(s string) (optBuyType, error) { var e optBuyType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type sellType uint // SellType* constants represent types of sales const ( SellTypeSell sellType = 1 + iota SellTypeSellShort ) var sellTypes = [...]string{"SELL", "SELLSHORT"} func (e sellType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SellTypeSell && e <= SellTypeSellShort } func (e sellType) String() string { if e.Valid() { return sellTypes[e-1] } return fmt.Sprintf("invalid sellType (%d)", e) } func (e *sellType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range sellTypes { if s == value { *e = sellType(i + 1) return nil } } *e = 0 return errors.New("Invalid SellType: \"" + in + "\"") } func (e *sellType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e sellType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(sellTypes[e-1], start) return nil } // NewSellType returns returns an 'enum' value of type sellType given its // string representation func NewSellType(s string) (sellType, error) { var e sellType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type loanPmtFreq uint // LoanPmtFreq* constants represent the frequency of loan payments const ( LoanPmtFreqWeekly loanPmtFreq = 1 + iota LoanPmtFreqBiweekly LoanPmtFreqTwiceMonthly LoanPmtFreqMonthly LoanPmtFreqFourWeeks LoanPmtFreqBiMonthly LoanPmtFreqQuarterly LoanPmtFreqSemiannually LoanPmtFreqAnnually LoanPmtFreqOther ) var loanPmtFreqs = [...]string{"WEEKLY", "BIWEEKLY", "TWICEMONTHLY", "MONTHLY", "FOURWEEKS", "BIMONTHLY", "QUARTERLY", "SEMIANNUALLY", "ANNUALLY", "OTHER"} func (e loanPmtFreq) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= LoanPmtFreqWeekly && e <= LoanPmtFreqOther } func (e loanPmtFreq) String() string { if e.Valid() { return loanPmtFreqs[e-1] } return fmt.Sprintf("invalid loanPmtFreq (%d)", e) } func (e *loanPmtFreq) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range loanPmtFreqs { if s == value { *e = loanPmtFreq(i + 1) return nil } } *e = 0 return errors.New("Invalid LoanPmtFreq: \"" + in + "\"") } func (e *loanPmtFreq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e loanPmtFreq) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(loanPmtFreqs[e-1], start) return nil } // NewLoanPmtFreq returns returns an 'enum' value of type loanPmtFreq given its // string representation func NewLoanPmtFreq(s string) (loanPmtFreq, error) { var e loanPmtFreq err := e.FromString(s) if err != nil { return 0, err } return e, nil } type incomeType uint // IncomeType* constants represent types of investment income const ( IncomeTypeCGLong incomeType = 1 + iota IncomeTypeCGShort IncomeTypeDiv IncomeTypeInterest IncomeTypeMisc ) var incomeTypes = [...]string{"CGLONG", "CGSHORT", "DIV", "INTEREST", "MISC"} func (e incomeType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= IncomeTypeCGLong && e <= IncomeTypeMisc } func (e incomeType) String() string { if e.Valid() { return incomeTypes[e-1] } return fmt.Sprintf("invalid incomeType (%d)", e) } func (e *incomeType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range incomeTypes { if s == value { *e = incomeType(i + 1) return nil } } *e = 0 return errors.New("Invalid IncomeType: \"" + in + "\"") } func (e *incomeType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e incomeType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(incomeTypes[e-1], start) return nil } // NewIncomeType returns returns an 'enum' value of type incomeType given its // string representation func NewIncomeType(s string) (incomeType, error) { var e incomeType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type sellReason uint // SellReason* constants represent the reason the sell of a debt security was generated: CALL (the debt was called), SELL (the debt was sold), MATURITY (the debt reached maturity) const ( SellReasonCall sellReason = 1 + iota SellReasonSell SellReasonMaturity ) var sellReasons = [...]string{"CALL", "SELL", "MATURITY"} func (e sellReason) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SellReasonCall && e <= SellReasonMaturity } func (e sellReason) String() string { if e.Valid() { return sellReasons[e-1] } return fmt.Sprintf("invalid sellReason (%d)", e) } func (e *sellReason) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range sellReasons { if s == value { *e = sellReason(i + 1) return nil } } *e = 0 return errors.New("Invalid SellReason: \"" + in + "\"") } func (e *sellReason) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e sellReason) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(sellReasons[e-1], start) return nil } // NewSellReason returns returns an 'enum' value of type sellReason given its // string representation func NewSellReason(s string) (sellReason, error) { var e sellReason err := e.FromString(s) if err != nil { return 0, err } return e, nil } type optSellType uint // OptSellType* constants represent types of sales for options const ( OptSellTypeSellToClose optSellType = 1 + iota OptSellTypeSellToOpen ) var optSellTypes = [...]string{"SELLTOCLOSE", "SELLTOOPEN"} func (e optSellType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OptSellTypeSellToClose && e <= OptSellTypeSellToOpen } func (e optSellType) String() string { if e.Valid() { return optSellTypes[e-1] } return fmt.Sprintf("invalid optSellType (%d)", e) } func (e *optSellType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range optSellTypes { if s == value { *e = optSellType(i + 1) return nil } } *e = 0 return errors.New("Invalid OptSellType: \"" + in + "\"") } func (e *optSellType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e optSellType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(optSellTypes[e-1], start) return nil } // NewOptSellType returns returns an 'enum' value of type optSellType given its // string representation func NewOptSellType(s string) (optSellType, error) { var e optSellType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type relType uint // RelType* constants represent related option transaction types const ( RelTypeSpread relType = 1 + iota RelTypeStraddle RelTypeNone RelTypeOther ) var relTypes = [...]string{"SPREAD", "STRADDLE", "NONE", "OTHER"} func (e relType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= RelTypeSpread && e <= RelTypeOther } func (e relType) String() string { if e.Valid() { return relTypes[e-1] } return fmt.Sprintf("invalid relType (%d)", e) } func (e *relType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range relTypes { if s == value { *e = relType(i + 1) return nil } } *e = 0 return errors.New("Invalid RelType: \"" + in + "\"") } func (e *relType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e relType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(relTypes[e-1], start) return nil } // NewRelType returns returns an 'enum' value of type relType given its // string representation func NewRelType(s string) (relType, error) { var e relType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type charType uint // CharType* constants represent types of characters allowed in password const ( CharTypeAlphaOnly charType = 1 + iota CharTypeNumericOnly CharTypeAlphaOrNumeric CharTypeAlphaAndNumeric ) var charTypes = [...]string{"ALPHAONLY", "NUMERICONLY", "ALPHAORNUMERIC", "ALPHAANDNUMERIC"} func (e charType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= CharTypeAlphaOnly && e <= CharTypeAlphaAndNumeric } func (e charType) String() string { if e.Valid() { return charTypes[e-1] } return fmt.Sprintf("invalid charType (%d)", e) } func (e *charType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range charTypes { if s == value { *e = charType(i + 1) return nil } } *e = 0 return errors.New("Invalid CharType: \"" + in + "\"") } func (e *charType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e charType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(charTypes[e-1], start) return nil } // NewCharType returns returns an 'enum' value of type charType given its // string representation func NewCharType(s string) (charType, error) { var e charType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type syncMode uint // SyncMode* constants represent data synchronization mode supported (see OFX spec for more details) const ( SyncModeFull syncMode = 1 + iota SyncModeLite ) var syncModes = [...]string{"FULL", "LITE"} func (e syncMode) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SyncModeFull && e <= SyncModeLite } func (e syncMode) String() string { if e.Valid() { return syncModes[e-1] } return fmt.Sprintf("invalid syncMode (%d)", e) } func (e *syncMode) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range syncModes { if s == value { *e = syncMode(i + 1) return nil } } *e = 0 return errors.New("Invalid SyncMode: \"" + in + "\"") } func (e *syncMode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e syncMode) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(syncModes[e-1], start) return nil } // NewSyncMode returns returns an 'enum' value of type syncMode given its // string representation func NewSyncMode(s string) (syncMode, error) { var e syncMode err := e.FromString(s) if err != nil { return 0, err } return e, nil } type ofxSec uint // OfxSec* constants represent the type of application-level security required for the message set const ( OfxSecNone ofxSec = 1 + iota OfxSecType1 ) var ofxSecs = [...]string{"NONE", "TYPE 1"} func (e ofxSec) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OfxSecNone && e <= OfxSecType1 } func (e ofxSec) String() string { if e.Valid() { return ofxSecs[e-1] } return fmt.Sprintf("invalid ofxSec (%d)", e) } func (e *ofxSec) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range ofxSecs { if s == value { *e = ofxSec(i + 1) return nil } } *e = 0 return errors.New("Invalid OfxSec: \"" + in + "\"") } func (e *ofxSec) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e ofxSec) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(ofxSecs[e-1], start) return nil } // NewOfxSec returns returns an 'enum' value of type ofxSec given its // string representation func NewOfxSec(s string) (ofxSec, error) { var e ofxSec err := e.FromString(s) if err != nil { return 0, err } return e, nil } type debtType uint // DebtType* constants represent debt type const ( DebtTypeCoupon debtType = 1 + iota DebtTypeZero ) var debtTypes = [...]string{"COUPON", "ZERO"} func (e debtType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= DebtTypeCoupon && e <= DebtTypeZero } func (e debtType) String() string { if e.Valid() { return debtTypes[e-1] } return fmt.Sprintf("invalid debtType (%d)", e) } func (e *debtType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range debtTypes { if s == value { *e = debtType(i + 1) return nil } } *e = 0 return errors.New("Invalid DebtType: \"" + in + "\"") } func (e *debtType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e debtType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(debtTypes[e-1], start) return nil } // NewDebtType returns returns an 'enum' value of type debtType given its // string representation func NewDebtType(s string) (debtType, error) { var e debtType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type debtClass uint // DebtClass* constants represent the class of debt const ( DebtClassTreasury debtClass = 1 + iota DebtClassMunicipal DebtClassCorporate DebtClassOther ) var debtClasss = [...]string{"TREASURY", "MUNICIPAL", "CORPORATE", "OTHER"} func (e debtClass) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= DebtClassTreasury && e <= DebtClassOther } func (e debtClass) String() string { if e.Valid() { return debtClasss[e-1] } return fmt.Sprintf("invalid debtClass (%d)", e) } func (e *debtClass) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range debtClasss { if s == value { *e = debtClass(i + 1) return nil } } *e = 0 return errors.New("Invalid DebtClass: \"" + in + "\"") } func (e *debtClass) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e debtClass) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(debtClasss[e-1], start) return nil } // NewDebtClass returns returns an 'enum' value of type debtClass given its // string representation func NewDebtClass(s string) (debtClass, error) { var e debtClass err := e.FromString(s) if err != nil { return 0, err } return e, nil } type couponFreq uint // CouponFreq* constants represent when debt coupons mature const ( CouponFreqMonthly couponFreq = 1 + iota CouponFreqQuarterly CouponFreqSemiannual CouponFreqAnnual CouponFreqOther ) var couponFreqs = [...]string{"MONTHLY", "QUARTERLY", "SEMIANNUAL", "ANNUAL", "OTHER"} func (e couponFreq) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= CouponFreqMonthly && e <= CouponFreqOther } func (e couponFreq) String() string { if e.Valid() { return couponFreqs[e-1] } return fmt.Sprintf("invalid couponFreq (%d)", e) } func (e *couponFreq) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range couponFreqs { if s == value { *e = couponFreq(i + 1) return nil } } *e = 0 return errors.New("Invalid CouponFreq: \"" + in + "\"") } func (e *couponFreq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e couponFreq) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(couponFreqs[e-1], start) return nil } // NewCouponFreq returns returns an 'enum' value of type couponFreq given its // string representation func NewCouponFreq(s string) (couponFreq, error) { var e couponFreq err := e.FromString(s) if err != nil { return 0, err } return e, nil } type callType uint // CallType* constants represent type of next call (for a debt) const ( CallTypeCall callType = 1 + iota CallTypePut CallTypePrefund CallTypeMaturity ) var callTypes = [...]string{"CALL", "PUT", "PREFUND", "MATURITY"} func (e callType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= CallTypeCall && e <= CallTypeMaturity } func (e callType) String() string { if e.Valid() { return callTypes[e-1] } return fmt.Sprintf("invalid callType (%d)", e) } func (e *callType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range callTypes { if s == value { *e = callType(i + 1) return nil } } *e = 0 return errors.New("Invalid CallType: \"" + in + "\"") } func (e *callType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e callType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(callTypes[e-1], start) return nil } // NewCallType returns returns an 'enum' value of type callType given its // string representation func NewCallType(s string) (callType, error) { var e callType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type assetClass uint // AssetClass* constants represent type of asset classes const ( AssetClassDomesticBond assetClass = 1 + iota AssetClassIntlBond AssetClassLargeStock AssetClassSmallStock AssetClassIntlStock AssetClassMoneyMrkt AssetClassOther ) var assetClasss = [...]string{"DOMESTICBOND", "INTLBOND", "LARGESTOCK", "SMALLSTOCK", "INTLSTOCK", "MONEYMRKT", "OTHER"} func (e assetClass) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= AssetClassDomesticBond && e <= AssetClassOther } func (e assetClass) String() string { if e.Valid() { return assetClasss[e-1] } return fmt.Sprintf("invalid assetClass (%d)", e) } func (e *assetClass) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range assetClasss { if s == value { *e = assetClass(i + 1) return nil } } *e = 0 return errors.New("Invalid AssetClass: \"" + in + "\"") } func (e *assetClass) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e assetClass) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(assetClasss[e-1], start) return nil } // NewAssetClass returns returns an 'enum' value of type assetClass given its // string representation func NewAssetClass(s string) (assetClass, error) { var e assetClass err := e.FromString(s) if err != nil { return 0, err } return e, nil } type mfType uint // MfType* constants represent types of mutual funds const ( MfTypeOpenEnd mfType = 1 + iota MfTypeCloseEnd MfTypeOther ) var mfTypes = [...]string{"OPENEND", "CLOSEEND", "OTHER"} func (e mfType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= MfTypeOpenEnd && e <= MfTypeOther } func (e mfType) String() string { if e.Valid() { return mfTypes[e-1] } return fmt.Sprintf("invalid mfType (%d)", e) } func (e *mfType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range mfTypes { if s == value { *e = mfType(i + 1) return nil } } *e = 0 return errors.New("Invalid MfType: \"" + in + "\"") } func (e *mfType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e mfType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(mfTypes[e-1], start) return nil } // NewMfType returns returns an 'enum' value of type mfType given its // string representation func NewMfType(s string) (mfType, error) { var e mfType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type optType uint // OptType* constants represent whether the option is a PUT or a CALL const ( OptTypePut optType = 1 + iota OptTypeCall ) var optTypes = [...]string{"PUT", "CALL"} func (e optType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= OptTypePut && e <= OptTypeCall } func (e optType) String() string { if e.Valid() { return optTypes[e-1] } return fmt.Sprintf("invalid optType (%d)", e) } func (e *optType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range optTypes { if s == value { *e = optType(i + 1) return nil } } *e = 0 return errors.New("Invalid OptType: \"" + in + "\"") } func (e *optType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e optType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(optTypes[e-1], start) return nil } // NewOptType returns returns an 'enum' value of type optType given its // string representation func NewOptType(s string) (optType, error) { var e optType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type stockType uint // StockType* constants represent types of stock const ( StockTypeCommon stockType = 1 + iota StockTypePreferred StockTypeConvertible StockTypeOther ) var stockTypes = [...]string{"COMMON", "PREFERRED", "CONVERTIBLE", "OTHER"} func (e stockType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= StockTypeCommon && e <= StockTypeOther } func (e stockType) String() string { if e.Valid() { return stockTypes[e-1] } return fmt.Sprintf("invalid stockType (%d)", e) } func (e *stockType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range stockTypes { if s == value { *e = stockType(i + 1) return nil } } *e = 0 return errors.New("Invalid StockType: \"" + in + "\"") } func (e *stockType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e stockType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(stockTypes[e-1], start) return nil } // NewStockType returns returns an 'enum' value of type stockType given its // string representation func NewStockType(s string) (stockType, error) { var e stockType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type holderType uint // HolderType* constants represent how the account is held const ( HolderTypeIndividual holderType = 1 + iota HolderTypeJoint HolderTypeCustodial HolderTypeTrust HolderTypeOther ) var holderTypes = [...]string{"INDIVIDUAL", "JOINT", "CUSTODIAL", "TRUST", "OTHER"} func (e holderType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= HolderTypeIndividual && e <= HolderTypeOther } func (e holderType) String() string { if e.Valid() { return holderTypes[e-1] } return fmt.Sprintf("invalid holderType (%d)", e) } func (e *holderType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range holderTypes { if s == value { *e = holderType(i + 1) return nil } } *e = 0 return errors.New("Invalid HolderType: \"" + in + "\"") } func (e *holderType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e holderType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(holderTypes[e-1], start) return nil } // NewHolderType returns returns an 'enum' value of type holderType given its // string representation func NewHolderType(s string) (holderType, error) { var e holderType err := e.FromString(s) if err != nil { return 0, err } return e, nil } type acctClassification uint // AcctClassification* constants represent the type of an account const ( AcctClassificationPersonal acctClassification = 1 + iota AcctClassificationBusiness AcctClassificationCorporate AcctClassificationOther ) var acctClassifications = [...]string{"PERSONAL", "BUSINESS", "CORPORATE", "OTHER"} func (e acctClassification) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= AcctClassificationPersonal && e <= AcctClassificationOther } func (e acctClassification) String() string { if e.Valid() { return acctClassifications[e-1] } return fmt.Sprintf("invalid acctClassification (%d)", e) } func (e *acctClassification) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range acctClassifications { if s == value { *e = acctClassification(i + 1) return nil } } *e = 0 return errors.New("Invalid AcctClassification: \"" + in + "\"") } func (e *acctClassification) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e acctClassification) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(acctClassifications[e-1], start) return nil } // NewAcctClassification returns returns an 'enum' value of type acctClassification given its // string representation func NewAcctClassification(s string) (acctClassification, error) { var e acctClassification err := e.FromString(s) if err != nil { return 0, err } return e, nil } type svcStatus uint // SvcStatus* constants represent the status of the account: AVAIL = Available, but not yet requested, PEND = Requested, but not yet available, ACTIVE = In use const ( SvcStatusAvail svcStatus = 1 + iota SvcStatusPend SvcStatusActive ) var svcStatuss = [...]string{"AVAIL", "PEND", "ACTIVE"} func (e svcStatus) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= SvcStatusAvail && e <= SvcStatusActive } func (e svcStatus) String() string { if e.Valid() { return svcStatuss[e-1] } return fmt.Sprintf("invalid svcStatus (%d)", e) } func (e *svcStatus) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range svcStatuss { if s == value { *e = svcStatus(i + 1) return nil } } *e = 0 return errors.New("Invalid SvcStatus: \"" + in + "\"") } func (e *svcStatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e svcStatus) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(svcStatuss[e-1], start) return nil } // NewSvcStatus returns returns an 'enum' value of type svcStatus given its // string representation func NewSvcStatus(s string) (svcStatus, error) { var e svcStatus err := e.FromString(s) if err != nil { return 0, err } return e, nil } type usProductType uint // UsProductType* constants represent type of investment account (in the US) const ( UsProductType401K usProductType = 1 + iota UsProductType403B UsProductTypeIRA UsProductTypeKEOGH UsProductTypeOther UsProductTypeSARSEP UsProductTypeSimple UsProductTypeNormal UsProductTypeTDA UsProductTypeTrust UsProductTypeUGMA ) var usProductTypes = [...]string{"401K", "403B", "IRA", "KEOGH", "OTHER", "SARSEP", "SIMPLE", "NORMAL", "TDA", "TRUST", "UGMA"} func (e usProductType) Valid() bool { // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= UsProductType401K && e <= UsProductTypeUGMA } func (e usProductType) String() string { if e.Valid() { return usProductTypes[e-1] } return fmt.Sprintf("invalid usProductType (%d)", e) } func (e *usProductType) FromString(in string) error { value := strings.TrimSpace(in) for i, s := range usProductTypes { if s == value { *e = usProductType(i + 1) return nil } } *e = 0 return errors.New("Invalid UsProductType: \"" + in + "\"") } func (e *usProductType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } return e.FromString(value) } func (e usProductType) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { if !e.Valid() { return nil } enc.EncodeElement(usProductTypes[e-1], start) return nil } // NewUsProductType returns returns an 'enum' value of type usProductType given its // string representation func NewUsProductType(s string) (usProductType, error) { var e usProductType err := e.FromString(s) if err != nil { return 0, err } return e, nil } ================================================ FILE: constants_test.go ================================================ package ofxgo /* * Do not edit this file by hand. It is auto-generated by calling `go generate`. * To make changes, edit generate_constants.py, re-run `go generate`, and check * in the result. */ import ( "strings" "testing" "github.com/aclindsa/xml" ) func TestOfxVersion(t *testing.T) { e, err := NewOfxVersion("102") if err != nil { t.Fatalf("Unexpected error creating new OfxVersion from string \"102\"\n") } if !e.Valid() { t.Fatalf("OfxVersion unexpectedly invalid\n") } err = e.FromString("220") if err != nil { t.Fatalf("Unexpected error on OfxVersion.FromString(\"220\")\n") } if e.String() != "220" { t.Fatalf("OfxVersion.String() expected to be \"220\"\n") } marshalHelper(t, "220", &e) overwritten, err := NewOfxVersion("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OfxVersion from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OfxVersion created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OfxVersion created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OfxVersion): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "220", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E ofxVersion } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OfxVersion): %s\n", err) } if string(b) != "220" { t.Fatalf("Expected '%s', got '%s'\n", "220", string(b)) } } func TestAcctType(t *testing.T) { e, err := NewAcctType("CHECKING") if err != nil { t.Fatalf("Unexpected error creating new AcctType from string \"CHECKING\"\n") } if !e.Valid() { t.Fatalf("AcctType unexpectedly invalid\n") } err = e.FromString("CD") if err != nil { t.Fatalf("Unexpected error on AcctType.FromString(\"CD\")\n") } if e.String() != "CD" { t.Fatalf("AcctType.String() expected to be \"CD\"\n") } marshalHelper(t, "CD", &e) overwritten, err := NewAcctType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new AcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("AcctType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("AcctType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(AcctType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "CD", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E acctType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct AcctType): %s\n", err) } if string(b) != "CD" { t.Fatalf("Expected '%s', got '%s'\n", "CD", string(b)) } } func TestTrnType(t *testing.T) { e, err := NewTrnType("CREDIT") if err != nil { t.Fatalf("Unexpected error creating new TrnType from string \"CREDIT\"\n") } if !e.Valid() { t.Fatalf("TrnType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on TrnType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("TrnType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewTrnType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new TrnType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("TrnType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("TrnType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(TrnType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E trnType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct TrnType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestImageType(t *testing.T) { e, err := NewImageType("STATEMENT") if err != nil { t.Fatalf("Unexpected error creating new ImageType from string \"STATEMENT\"\n") } if !e.Valid() { t.Fatalf("ImageType unexpectedly invalid\n") } err = e.FromString("TAX") if err != nil { t.Fatalf("Unexpected error on ImageType.FromString(\"TAX\")\n") } if e.String() != "TAX" { t.Fatalf("ImageType.String() expected to be \"TAX\"\n") } marshalHelper(t, "TAX", &e) overwritten, err := NewImageType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new ImageType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("ImageType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("ImageType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(ImageType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "TAX", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E imageType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct ImageType): %s\n", err) } if string(b) != "TAX" { t.Fatalf("Expected '%s', got '%s'\n", "TAX", string(b)) } } func TestImageRefType(t *testing.T) { e, err := NewImageRefType("OPAQUE") if err != nil { t.Fatalf("Unexpected error creating new ImageRefType from string \"OPAQUE\"\n") } if !e.Valid() { t.Fatalf("ImageRefType unexpectedly invalid\n") } err = e.FromString("FORMURL") if err != nil { t.Fatalf("Unexpected error on ImageRefType.FromString(\"FORMURL\")\n") } if e.String() != "FORMURL" { t.Fatalf("ImageRefType.String() expected to be \"FORMURL\"\n") } marshalHelper(t, "FORMURL", &e) overwritten, err := NewImageRefType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new ImageRefType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("ImageRefType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("ImageRefType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(ImageRefType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "FORMURL", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E imageRefType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct ImageRefType): %s\n", err) } if string(b) != "FORMURL" { t.Fatalf("Expected '%s', got '%s'\n", "FORMURL", string(b)) } } func TestCheckSup(t *testing.T) { e, err := NewCheckSup("FRONTONLY") if err != nil { t.Fatalf("Unexpected error creating new CheckSup from string \"FRONTONLY\"\n") } if !e.Valid() { t.Fatalf("CheckSup unexpectedly invalid\n") } err = e.FromString("FRONTANDBACK") if err != nil { t.Fatalf("Unexpected error on CheckSup.FromString(\"FRONTANDBACK\")\n") } if e.String() != "FRONTANDBACK" { t.Fatalf("CheckSup.String() expected to be \"FRONTANDBACK\"\n") } marshalHelper(t, "FRONTANDBACK", &e) overwritten, err := NewCheckSup("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new CheckSup from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("CheckSup created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("CheckSup created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(CheckSup): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "FRONTANDBACK", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E checkSup } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct CheckSup): %s\n", err) } if string(b) != "FRONTANDBACK" { t.Fatalf("Expected '%s', got '%s'\n", "FRONTANDBACK", string(b)) } } func TestCorrectAction(t *testing.T) { e, err := NewCorrectAction("DELETE") if err != nil { t.Fatalf("Unexpected error creating new CorrectAction from string \"DELETE\"\n") } if !e.Valid() { t.Fatalf("CorrectAction unexpectedly invalid\n") } err = e.FromString("REPLACE") if err != nil { t.Fatalf("Unexpected error on CorrectAction.FromString(\"REPLACE\")\n") } if e.String() != "REPLACE" { t.Fatalf("CorrectAction.String() expected to be \"REPLACE\"\n") } marshalHelper(t, "REPLACE", &e) overwritten, err := NewCorrectAction("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new CorrectAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("CorrectAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("CorrectAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(CorrectAction): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "REPLACE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E correctAction } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct CorrectAction): %s\n", err) } if string(b) != "REPLACE" { t.Fatalf("Expected '%s', got '%s'\n", "REPLACE", string(b)) } } func TestBalType(t *testing.T) { e, err := NewBalType("DOLLAR") if err != nil { t.Fatalf("Unexpected error creating new BalType from string \"DOLLAR\"\n") } if !e.Valid() { t.Fatalf("BalType unexpectedly invalid\n") } err = e.FromString("NUMBER") if err != nil { t.Fatalf("Unexpected error on BalType.FromString(\"NUMBER\")\n") } if e.String() != "NUMBER" { t.Fatalf("BalType.String() expected to be \"NUMBER\"\n") } marshalHelper(t, "NUMBER", &e) overwritten, err := NewBalType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new BalType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("BalType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("BalType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(BalType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "NUMBER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E balType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct BalType): %s\n", err) } if string(b) != "NUMBER" { t.Fatalf("Expected '%s', got '%s'\n", "NUMBER", string(b)) } } func TestInv401kSource(t *testing.T) { e, err := NewInv401kSource("PRETAX") if err != nil { t.Fatalf("Unexpected error creating new Inv401kSource from string \"PRETAX\"\n") } if !e.Valid() { t.Fatalf("Inv401kSource unexpectedly invalid\n") } err = e.FromString("OTHERNONVEST") if err != nil { t.Fatalf("Unexpected error on Inv401kSource.FromString(\"OTHERNONVEST\")\n") } if e.String() != "OTHERNONVEST" { t.Fatalf("Inv401kSource.String() expected to be \"OTHERNONVEST\"\n") } marshalHelper(t, "OTHERNONVEST", &e) overwritten, err := NewInv401kSource("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new Inv401kSource from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("Inv401kSource created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("Inv401kSource created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(Inv401kSource): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHERNONVEST", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E inv401kSource } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct Inv401kSource): %s\n", err) } if string(b) != "OTHERNONVEST" { t.Fatalf("Expected '%s', got '%s'\n", "OTHERNONVEST", string(b)) } } func TestSubAcctType(t *testing.T) { e, err := NewSubAcctType("CASH") if err != nil { t.Fatalf("Unexpected error creating new SubAcctType from string \"CASH\"\n") } if !e.Valid() { t.Fatalf("SubAcctType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on SubAcctType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("SubAcctType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewSubAcctType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new SubAcctType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("SubAcctType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("SubAcctType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(SubAcctType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E subAcctType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct SubAcctType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestBuyType(t *testing.T) { e, err := NewBuyType("BUY") if err != nil { t.Fatalf("Unexpected error creating new BuyType from string \"BUY\"\n") } if !e.Valid() { t.Fatalf("BuyType unexpectedly invalid\n") } err = e.FromString("BUYTOCOVER") if err != nil { t.Fatalf("Unexpected error on BuyType.FromString(\"BUYTOCOVER\")\n") } if e.String() != "BUYTOCOVER" { t.Fatalf("BuyType.String() expected to be \"BUYTOCOVER\"\n") } marshalHelper(t, "BUYTOCOVER", &e) overwritten, err := NewBuyType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new BuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("BuyType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("BuyType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(BuyType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "BUYTOCOVER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E buyType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct BuyType): %s\n", err) } if string(b) != "BUYTOCOVER" { t.Fatalf("Expected '%s', got '%s'\n", "BUYTOCOVER", string(b)) } } func TestOptAction(t *testing.T) { e, err := NewOptAction("EXERCISE") if err != nil { t.Fatalf("Unexpected error creating new OptAction from string \"EXERCISE\"\n") } if !e.Valid() { t.Fatalf("OptAction unexpectedly invalid\n") } err = e.FromString("EXPIRE") if err != nil { t.Fatalf("Unexpected error on OptAction.FromString(\"EXPIRE\")\n") } if e.String() != "EXPIRE" { t.Fatalf("OptAction.String() expected to be \"EXPIRE\"\n") } marshalHelper(t, "EXPIRE", &e) overwritten, err := NewOptAction("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OptAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OptAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OptAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OptAction): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "EXPIRE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E optAction } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OptAction): %s\n", err) } if string(b) != "EXPIRE" { t.Fatalf("Expected '%s', got '%s'\n", "EXPIRE", string(b)) } } func TestTferAction(t *testing.T) { e, err := NewTferAction("IN") if err != nil { t.Fatalf("Unexpected error creating new TferAction from string \"IN\"\n") } if !e.Valid() { t.Fatalf("TferAction unexpectedly invalid\n") } err = e.FromString("OUT") if err != nil { t.Fatalf("Unexpected error on TferAction.FromString(\"OUT\")\n") } if e.String() != "OUT" { t.Fatalf("TferAction.String() expected to be \"OUT\"\n") } marshalHelper(t, "OUT", &e) overwritten, err := NewTferAction("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new TferAction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("TferAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("TferAction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(TferAction): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OUT", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E tferAction } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct TferAction): %s\n", err) } if string(b) != "OUT" { t.Fatalf("Expected '%s', got '%s'\n", "OUT", string(b)) } } func TestPosType(t *testing.T) { e, err := NewPosType("LONG") if err != nil { t.Fatalf("Unexpected error creating new PosType from string \"LONG\"\n") } if !e.Valid() { t.Fatalf("PosType unexpectedly invalid\n") } err = e.FromString("SHORT") if err != nil { t.Fatalf("Unexpected error on PosType.FromString(\"SHORT\")\n") } if e.String() != "SHORT" { t.Fatalf("PosType.String() expected to be \"SHORT\"\n") } marshalHelper(t, "SHORT", &e) overwritten, err := NewPosType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new PosType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("PosType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("PosType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(PosType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "SHORT", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E posType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct PosType): %s\n", err) } if string(b) != "SHORT" { t.Fatalf("Expected '%s', got '%s'\n", "SHORT", string(b)) } } func TestSecured(t *testing.T) { e, err := NewSecured("NAKED") if err != nil { t.Fatalf("Unexpected error creating new Secured from string \"NAKED\"\n") } if !e.Valid() { t.Fatalf("Secured unexpectedly invalid\n") } err = e.FromString("COVERED") if err != nil { t.Fatalf("Unexpected error on Secured.FromString(\"COVERED\")\n") } if e.String() != "COVERED" { t.Fatalf("Secured.String() expected to be \"COVERED\"\n") } marshalHelper(t, "COVERED", &e) overwritten, err := NewSecured("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new Secured from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("Secured created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("Secured created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(Secured): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "COVERED", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E secured } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct Secured): %s\n", err) } if string(b) != "COVERED" { t.Fatalf("Expected '%s', got '%s'\n", "COVERED", string(b)) } } func TestDuration(t *testing.T) { e, err := NewDuration("DAY") if err != nil { t.Fatalf("Unexpected error creating new Duration from string \"DAY\"\n") } if !e.Valid() { t.Fatalf("Duration unexpectedly invalid\n") } err = e.FromString("IMMEDIATE") if err != nil { t.Fatalf("Unexpected error on Duration.FromString(\"IMMEDIATE\")\n") } if e.String() != "IMMEDIATE" { t.Fatalf("Duration.String() expected to be \"IMMEDIATE\"\n") } marshalHelper(t, "IMMEDIATE", &e) overwritten, err := NewDuration("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new Duration from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("Duration created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("Duration created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(Duration): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "IMMEDIATE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E duration } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct Duration): %s\n", err) } if string(b) != "IMMEDIATE" { t.Fatalf("Expected '%s', got '%s'\n", "IMMEDIATE", string(b)) } } func TestRestriction(t *testing.T) { e, err := NewRestriction("ALLORNONE") if err != nil { t.Fatalf("Unexpected error creating new Restriction from string \"ALLORNONE\"\n") } if !e.Valid() { t.Fatalf("Restriction unexpectedly invalid\n") } err = e.FromString("NONE") if err != nil { t.Fatalf("Unexpected error on Restriction.FromString(\"NONE\")\n") } if e.String() != "NONE" { t.Fatalf("Restriction.String() expected to be \"NONE\"\n") } marshalHelper(t, "NONE", &e) overwritten, err := NewRestriction("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new Restriction from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("Restriction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("Restriction created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(Restriction): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "NONE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E restriction } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct Restriction): %s\n", err) } if string(b) != "NONE" { t.Fatalf("Expected '%s', got '%s'\n", "NONE", string(b)) } } func TestUnitType(t *testing.T) { e, err := NewUnitType("SHARES") if err != nil { t.Fatalf("Unexpected error creating new UnitType from string \"SHARES\"\n") } if !e.Valid() { t.Fatalf("UnitType unexpectedly invalid\n") } err = e.FromString("CURRENCY") if err != nil { t.Fatalf("Unexpected error on UnitType.FromString(\"CURRENCY\")\n") } if e.String() != "CURRENCY" { t.Fatalf("UnitType.String() expected to be \"CURRENCY\"\n") } marshalHelper(t, "CURRENCY", &e) overwritten, err := NewUnitType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new UnitType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("UnitType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("UnitType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(UnitType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "CURRENCY", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E unitType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct UnitType): %s\n", err) } if string(b) != "CURRENCY" { t.Fatalf("Expected '%s', got '%s'\n", "CURRENCY", string(b)) } } func TestOptBuyType(t *testing.T) { e, err := NewOptBuyType("BUYTOOPEN") if err != nil { t.Fatalf("Unexpected error creating new OptBuyType from string \"BUYTOOPEN\"\n") } if !e.Valid() { t.Fatalf("OptBuyType unexpectedly invalid\n") } err = e.FromString("BUYTOCLOSE") if err != nil { t.Fatalf("Unexpected error on OptBuyType.FromString(\"BUYTOCLOSE\")\n") } if e.String() != "BUYTOCLOSE" { t.Fatalf("OptBuyType.String() expected to be \"BUYTOCLOSE\"\n") } marshalHelper(t, "BUYTOCLOSE", &e) overwritten, err := NewOptBuyType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OptBuyType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OptBuyType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OptBuyType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OptBuyType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "BUYTOCLOSE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E optBuyType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OptBuyType): %s\n", err) } if string(b) != "BUYTOCLOSE" { t.Fatalf("Expected '%s', got '%s'\n", "BUYTOCLOSE", string(b)) } } func TestSellType(t *testing.T) { e, err := NewSellType("SELL") if err != nil { t.Fatalf("Unexpected error creating new SellType from string \"SELL\"\n") } if !e.Valid() { t.Fatalf("SellType unexpectedly invalid\n") } err = e.FromString("SELLSHORT") if err != nil { t.Fatalf("Unexpected error on SellType.FromString(\"SELLSHORT\")\n") } if e.String() != "SELLSHORT" { t.Fatalf("SellType.String() expected to be \"SELLSHORT\"\n") } marshalHelper(t, "SELLSHORT", &e) overwritten, err := NewSellType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new SellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("SellType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("SellType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(SellType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "SELLSHORT", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E sellType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct SellType): %s\n", err) } if string(b) != "SELLSHORT" { t.Fatalf("Expected '%s', got '%s'\n", "SELLSHORT", string(b)) } } func TestLoanPmtFreq(t *testing.T) { e, err := NewLoanPmtFreq("WEEKLY") if err != nil { t.Fatalf("Unexpected error creating new LoanPmtFreq from string \"WEEKLY\"\n") } if !e.Valid() { t.Fatalf("LoanPmtFreq unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on LoanPmtFreq.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("LoanPmtFreq.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewLoanPmtFreq("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new LoanPmtFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("LoanPmtFreq created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("LoanPmtFreq created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(LoanPmtFreq): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E loanPmtFreq } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct LoanPmtFreq): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestIncomeType(t *testing.T) { e, err := NewIncomeType("CGLONG") if err != nil { t.Fatalf("Unexpected error creating new IncomeType from string \"CGLONG\"\n") } if !e.Valid() { t.Fatalf("IncomeType unexpectedly invalid\n") } err = e.FromString("MISC") if err != nil { t.Fatalf("Unexpected error on IncomeType.FromString(\"MISC\")\n") } if e.String() != "MISC" { t.Fatalf("IncomeType.String() expected to be \"MISC\"\n") } marshalHelper(t, "MISC", &e) overwritten, err := NewIncomeType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new IncomeType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("IncomeType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("IncomeType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(IncomeType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "MISC", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E incomeType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct IncomeType): %s\n", err) } if string(b) != "MISC" { t.Fatalf("Expected '%s', got '%s'\n", "MISC", string(b)) } } func TestSellReason(t *testing.T) { e, err := NewSellReason("CALL") if err != nil { t.Fatalf("Unexpected error creating new SellReason from string \"CALL\"\n") } if !e.Valid() { t.Fatalf("SellReason unexpectedly invalid\n") } err = e.FromString("MATURITY") if err != nil { t.Fatalf("Unexpected error on SellReason.FromString(\"MATURITY\")\n") } if e.String() != "MATURITY" { t.Fatalf("SellReason.String() expected to be \"MATURITY\"\n") } marshalHelper(t, "MATURITY", &e) overwritten, err := NewSellReason("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new SellReason from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("SellReason created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("SellReason created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(SellReason): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "MATURITY", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E sellReason } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct SellReason): %s\n", err) } if string(b) != "MATURITY" { t.Fatalf("Expected '%s', got '%s'\n", "MATURITY", string(b)) } } func TestOptSellType(t *testing.T) { e, err := NewOptSellType("SELLTOCLOSE") if err != nil { t.Fatalf("Unexpected error creating new OptSellType from string \"SELLTOCLOSE\"\n") } if !e.Valid() { t.Fatalf("OptSellType unexpectedly invalid\n") } err = e.FromString("SELLTOOPEN") if err != nil { t.Fatalf("Unexpected error on OptSellType.FromString(\"SELLTOOPEN\")\n") } if e.String() != "SELLTOOPEN" { t.Fatalf("OptSellType.String() expected to be \"SELLTOOPEN\"\n") } marshalHelper(t, "SELLTOOPEN", &e) overwritten, err := NewOptSellType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OptSellType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OptSellType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OptSellType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OptSellType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "SELLTOOPEN", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E optSellType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OptSellType): %s\n", err) } if string(b) != "SELLTOOPEN" { t.Fatalf("Expected '%s', got '%s'\n", "SELLTOOPEN", string(b)) } } func TestRelType(t *testing.T) { e, err := NewRelType("SPREAD") if err != nil { t.Fatalf("Unexpected error creating new RelType from string \"SPREAD\"\n") } if !e.Valid() { t.Fatalf("RelType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on RelType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("RelType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewRelType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new RelType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("RelType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("RelType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(RelType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E relType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct RelType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestCharType(t *testing.T) { e, err := NewCharType("ALPHAONLY") if err != nil { t.Fatalf("Unexpected error creating new CharType from string \"ALPHAONLY\"\n") } if !e.Valid() { t.Fatalf("CharType unexpectedly invalid\n") } err = e.FromString("ALPHAANDNUMERIC") if err != nil { t.Fatalf("Unexpected error on CharType.FromString(\"ALPHAANDNUMERIC\")\n") } if e.String() != "ALPHAANDNUMERIC" { t.Fatalf("CharType.String() expected to be \"ALPHAANDNUMERIC\"\n") } marshalHelper(t, "ALPHAANDNUMERIC", &e) overwritten, err := NewCharType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new CharType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("CharType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("CharType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(CharType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "ALPHAANDNUMERIC", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E charType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct CharType): %s\n", err) } if string(b) != "ALPHAANDNUMERIC" { t.Fatalf("Expected '%s', got '%s'\n", "ALPHAANDNUMERIC", string(b)) } } func TestSyncMode(t *testing.T) { e, err := NewSyncMode("FULL") if err != nil { t.Fatalf("Unexpected error creating new SyncMode from string \"FULL\"\n") } if !e.Valid() { t.Fatalf("SyncMode unexpectedly invalid\n") } err = e.FromString("LITE") if err != nil { t.Fatalf("Unexpected error on SyncMode.FromString(\"LITE\")\n") } if e.String() != "LITE" { t.Fatalf("SyncMode.String() expected to be \"LITE\"\n") } marshalHelper(t, "LITE", &e) overwritten, err := NewSyncMode("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new SyncMode from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("SyncMode created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("SyncMode created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(SyncMode): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "LITE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E syncMode } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct SyncMode): %s\n", err) } if string(b) != "LITE" { t.Fatalf("Expected '%s', got '%s'\n", "LITE", string(b)) } } func TestOfxSec(t *testing.T) { e, err := NewOfxSec("NONE") if err != nil { t.Fatalf("Unexpected error creating new OfxSec from string \"NONE\"\n") } if !e.Valid() { t.Fatalf("OfxSec unexpectedly invalid\n") } err = e.FromString("TYPE 1") if err != nil { t.Fatalf("Unexpected error on OfxSec.FromString(\"TYPE 1\")\n") } if e.String() != "TYPE 1" { t.Fatalf("OfxSec.String() expected to be \"TYPE 1\"\n") } marshalHelper(t, "TYPE 1", &e) overwritten, err := NewOfxSec("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OfxSec from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OfxSec created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OfxSec created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OfxSec): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "TYPE 1", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E ofxSec } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OfxSec): %s\n", err) } if string(b) != "TYPE 1" { t.Fatalf("Expected '%s', got '%s'\n", "TYPE 1", string(b)) } } func TestDebtType(t *testing.T) { e, err := NewDebtType("COUPON") if err != nil { t.Fatalf("Unexpected error creating new DebtType from string \"COUPON\"\n") } if !e.Valid() { t.Fatalf("DebtType unexpectedly invalid\n") } err = e.FromString("ZERO") if err != nil { t.Fatalf("Unexpected error on DebtType.FromString(\"ZERO\")\n") } if e.String() != "ZERO" { t.Fatalf("DebtType.String() expected to be \"ZERO\"\n") } marshalHelper(t, "ZERO", &e) overwritten, err := NewDebtType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new DebtType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("DebtType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("DebtType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(DebtType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "ZERO", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E debtType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct DebtType): %s\n", err) } if string(b) != "ZERO" { t.Fatalf("Expected '%s', got '%s'\n", "ZERO", string(b)) } } func TestDebtClass(t *testing.T) { e, err := NewDebtClass("TREASURY") if err != nil { t.Fatalf("Unexpected error creating new DebtClass from string \"TREASURY\"\n") } if !e.Valid() { t.Fatalf("DebtClass unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on DebtClass.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("DebtClass.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewDebtClass("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new DebtClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("DebtClass created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("DebtClass created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(DebtClass): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E debtClass } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct DebtClass): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestCouponFreq(t *testing.T) { e, err := NewCouponFreq("MONTHLY") if err != nil { t.Fatalf("Unexpected error creating new CouponFreq from string \"MONTHLY\"\n") } if !e.Valid() { t.Fatalf("CouponFreq unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on CouponFreq.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("CouponFreq.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewCouponFreq("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new CouponFreq from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("CouponFreq created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("CouponFreq created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(CouponFreq): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E couponFreq } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct CouponFreq): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestCallType(t *testing.T) { e, err := NewCallType("CALL") if err != nil { t.Fatalf("Unexpected error creating new CallType from string \"CALL\"\n") } if !e.Valid() { t.Fatalf("CallType unexpectedly invalid\n") } err = e.FromString("MATURITY") if err != nil { t.Fatalf("Unexpected error on CallType.FromString(\"MATURITY\")\n") } if e.String() != "MATURITY" { t.Fatalf("CallType.String() expected to be \"MATURITY\"\n") } marshalHelper(t, "MATURITY", &e) overwritten, err := NewCallType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new CallType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("CallType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("CallType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(CallType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "MATURITY", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E callType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct CallType): %s\n", err) } if string(b) != "MATURITY" { t.Fatalf("Expected '%s', got '%s'\n", "MATURITY", string(b)) } } func TestAssetClass(t *testing.T) { e, err := NewAssetClass("DOMESTICBOND") if err != nil { t.Fatalf("Unexpected error creating new AssetClass from string \"DOMESTICBOND\"\n") } if !e.Valid() { t.Fatalf("AssetClass unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on AssetClass.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("AssetClass.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewAssetClass("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new AssetClass from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("AssetClass created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("AssetClass created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(AssetClass): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E assetClass } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct AssetClass): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestMfType(t *testing.T) { e, err := NewMfType("OPENEND") if err != nil { t.Fatalf("Unexpected error creating new MfType from string \"OPENEND\"\n") } if !e.Valid() { t.Fatalf("MfType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on MfType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("MfType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewMfType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new MfType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("MfType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("MfType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(MfType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E mfType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct MfType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestOptType(t *testing.T) { e, err := NewOptType("PUT") if err != nil { t.Fatalf("Unexpected error creating new OptType from string \"PUT\"\n") } if !e.Valid() { t.Fatalf("OptType unexpectedly invalid\n") } err = e.FromString("CALL") if err != nil { t.Fatalf("Unexpected error on OptType.FromString(\"CALL\")\n") } if e.String() != "CALL" { t.Fatalf("OptType.String() expected to be \"CALL\"\n") } marshalHelper(t, "CALL", &e) overwritten, err := NewOptType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new OptType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("OptType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("OptType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(OptType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "CALL", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E optType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct OptType): %s\n", err) } if string(b) != "CALL" { t.Fatalf("Expected '%s', got '%s'\n", "CALL", string(b)) } } func TestStockType(t *testing.T) { e, err := NewStockType("COMMON") if err != nil { t.Fatalf("Unexpected error creating new StockType from string \"COMMON\"\n") } if !e.Valid() { t.Fatalf("StockType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on StockType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("StockType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewStockType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new StockType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("StockType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("StockType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(StockType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E stockType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct StockType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestHolderType(t *testing.T) { e, err := NewHolderType("INDIVIDUAL") if err != nil { t.Fatalf("Unexpected error creating new HolderType from string \"INDIVIDUAL\"\n") } if !e.Valid() { t.Fatalf("HolderType unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on HolderType.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("HolderType.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewHolderType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new HolderType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("HolderType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("HolderType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(HolderType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E holderType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct HolderType): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestAcctClassification(t *testing.T) { e, err := NewAcctClassification("PERSONAL") if err != nil { t.Fatalf("Unexpected error creating new AcctClassification from string \"PERSONAL\"\n") } if !e.Valid() { t.Fatalf("AcctClassification unexpectedly invalid\n") } err = e.FromString("OTHER") if err != nil { t.Fatalf("Unexpected error on AcctClassification.FromString(\"OTHER\")\n") } if e.String() != "OTHER" { t.Fatalf("AcctClassification.String() expected to be \"OTHER\"\n") } marshalHelper(t, "OTHER", &e) overwritten, err := NewAcctClassification("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new AcctClassification from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("AcctClassification created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("AcctClassification created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(AcctClassification): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "OTHER", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E acctClassification } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct AcctClassification): %s\n", err) } if string(b) != "OTHER" { t.Fatalf("Expected '%s', got '%s'\n", "OTHER", string(b)) } } func TestSvcStatus(t *testing.T) { e, err := NewSvcStatus("AVAIL") if err != nil { t.Fatalf("Unexpected error creating new SvcStatus from string \"AVAIL\"\n") } if !e.Valid() { t.Fatalf("SvcStatus unexpectedly invalid\n") } err = e.FromString("ACTIVE") if err != nil { t.Fatalf("Unexpected error on SvcStatus.FromString(\"ACTIVE\")\n") } if e.String() != "ACTIVE" { t.Fatalf("SvcStatus.String() expected to be \"ACTIVE\"\n") } marshalHelper(t, "ACTIVE", &e) overwritten, err := NewSvcStatus("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new SvcStatus from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("SvcStatus created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("SvcStatus created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(SvcStatus): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "ACTIVE", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E svcStatus } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct SvcStatus): %s\n", err) } if string(b) != "ACTIVE" { t.Fatalf("Expected '%s', got '%s'\n", "ACTIVE", string(b)) } } func TestUsProductType(t *testing.T) { e, err := NewUsProductType("401K") if err != nil { t.Fatalf("Unexpected error creating new UsProductType from string \"401K\"\n") } if !e.Valid() { t.Fatalf("UsProductType unexpectedly invalid\n") } err = e.FromString("UGMA") if err != nil { t.Fatalf("Unexpected error on UsProductType.FromString(\"UGMA\")\n") } if e.String() != "UGMA" { t.Fatalf("UsProductType.String() expected to be \"UGMA\"\n") } marshalHelper(t, "UGMA", &e) overwritten, err := NewUsProductType("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil { t.Fatalf("Expected error creating new UsProductType from string \"THISWILLNEVERBEAVALIDENUMSTRING\"\n") } if overwritten.Valid() { t.Fatalf("UsProductType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not be valid\n") } if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") { t.Fatalf("UsProductType created with string \"THISWILLNEVERBEAVALIDENUMSTRING\" should not return valid string from String()\n") } b, err := xml.Marshal(&overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(UsProductType): %s\n", err) } if string(b) != "" { t.Fatalf("Expected empty string, got '%s'\n", string(b)) } unmarshalHelper(t, "UGMA", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil { t.Fatalf("Expected error unmarshalling garbage value\n") } type SC struct { E usProductType } sc := SC{E: e} b, err = xml.Marshal(sc) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(struct UsProductType): %s\n", err) } if string(b) != "UGMA" { t.Fatalf("Expected '%s', got '%s'\n", "UGMA", string(b)) } } ================================================ FILE: creditcard.go ================================================ package ofxgo import ( "github.com/aclindsa/xml" ) // CCStatementRequest represents a request for a credit card statement. It is // used to request balances and/or transactions. See StatementRequest for the // analog for all other bank accounts. type CCStatementRequest struct { XMLName xml.Name `xml:"CCSTMTTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // TODO OFXEXTENSION CCAcctFrom CCAcct `xml:"CCSTMTRQ>CCACCTFROM"` DtStart *Date `xml:"CCSTMTRQ>INCTRAN>DTSTART,omitempty"` DtEnd *Date `xml:"CCSTMTRQ>INCTRAN>DTEND,omitempty"` Include Boolean `xml:"CCSTMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance) IncludePending Boolean `xml:"CCSTMTRQ>INCLUDEPENDING,omitempty"` // Include pending transactions IncTranImg Boolean `xml:"CCSTMTRQ>INCTRANIMG,omitempty"` // Include transaction images } // Name returns the name of the top-level transaction XML/SGML element func (r *CCStatementRequest) Name() string { return "CCSTMTTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *CCStatementRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement return true, nil } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *CCStatementRequest) Type() messageType { return CreditCardRq } // CCStatementResponse represents a credit card statement, including its // balances and possibly transactions. It is a response to CCStatementRequest, // or sometimes provided as part of an OFX file downloaded manually from an FI. type CCStatementResponse struct { XMLName xml.Name `xml:"CCSTMTTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` CurDef CurrSymbol `xml:"CCSTMTRS>CURDEF"` CCAcctFrom CCAcct `xml:"CCSTMTRS>CCACCTFROM"` BankTranList *TransactionList `xml:"CCSTMTRS>BANKTRANLIST,omitempty"` //BANKTRANLISTP BalAmt Amount `xml:"CCSTMTRS>LEDGERBAL>BALAMT"` DtAsOf Date `xml:"CCSTMTRS>LEDGERBAL>DTASOF"` AvailBalAmt *Amount `xml:"CCSTMTRS>AVAILBAL>BALAMT,omitempty"` AvailDtAsOf *Date `xml:"CCSTMTRS>AVAILBAL>DTASOF,omitempty"` CashAdvBalAmt Amount `xml:"CCSTMTRS>CASHADVBALAMT,omitempty"` // Only for CREDITLINE accounts, available balance for cash advances IntRatePurch Amount `xml:"CCSTMTRS>INTRATEPURCH,omitempty"` // Current interest rate for purchases IntRateCash Amount `xml:"CCSTMTRS>INTRATECASH,omitempty"` // Current interest rate for cash advances IntRateXfer Amount `xml:"CCSTMTRS>INTRATEXFER,omitempty"` // Current interest rate for cash advances RewardName String `xml:"CCSTMTRS>REWARDINFO>NAME,omitempty"` // Name of the reward program referred to by the next two elements RewardBal Amount `xml:"CCSTMTRS>REWARDINFO>REWARDBAL,omitempty"` // Current balance of the reward program RewardEarned Amount `xml:"CCSTMTRS>REWARDINFO>REWARDEARNED,omitempty"` // Reward amount earned YTD BalList []Balance `xml:"CCSTMTRS>BALLIST>BAL,omitempty"` MktgInfo String `xml:"CCSTMTRS>MKTGINFO,omitempty"` // Marketing information } // Name returns the name of the top-level transaction XML/SGML element func (sr *CCStatementResponse) Name() string { return "CCSTMTTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (sr *CCStatementResponse) Valid(version ofxVersion) (bool, error) { if ok, err := sr.TrnUID.Valid(); !ok { return false, err } //TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (sr *CCStatementResponse) Type() messageType { return CreditCardRs } ================================================ FILE: creditcard_test.go ================================================ package ofxgo import ( "strings" "testing" "time" ) func TestMarshalCCStatementRequest(t *testing.T) { var expectedString string = ` 20170331153848.000[0:GMT] myusername Pa$$word ENG BNK 1987 OFXGO 0001 913846 XXXXXXXXXXXX1234 20170101000000.000[0:GMT] Y ` var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "myusername" request.Signon.UserPass = "Pa$$word" request.Signon.Org = "BNK" request.Signon.Fid = "1987" statementRequest := CCStatementRequest{ TrnUID: "913846", CCAcctFrom: CCAcct{ AcctID: "XXXXXXXXXXXX1234", }, DtStart: NewDateGMT(2017, 1, 1, 0, 0, 0, 0), Include: true, } request.CreditCard = append(request.CreditCard, &statementRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() request.Signon.DtClient = *NewDateGMT(2017, 3, 31, 15, 38, 48, 0) marshalCheckRequest(t, &request, expectedString) } func TestUnmarshalCCStatementResponse102(t *testing.T) { responseReader := strings.NewReader(`OFXHEADER:100 DATA:OFXSGML VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 0INFOSUCCESS20170331154648.331[-4:EDT]ENG018172959e850ad-7448-b4ce-4b71-29057763b3060INFOUSD928374448846377520161201154648.688[-5:EST]20170331154648.688[-4:EDT]DEBIT20170209120000[0:GMT]-7.962017020924435657040207171600195SLICE OF NYCREDIT20161228120000[0:GMT]3830.462016122823633637200000258482730Payment Thank You ElectroDEBIT20170327120000[0:GMT]-17.72017032724445727085300442885680KROGER FUEL #9999-933420170331080000.000[-4:EDT]7630.1720170331080000.000[-4:EDT]`) var expected Response EDT := time.FixedZone("EDT", -4*60*60) EST := time.FixedZone("EST", -5*60*60) expected.Version = OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.Status.Message = "SUCCESS" expected.Signon.DtServer = *NewDate(2017, 3, 31, 15, 46, 48, 331000000, EDT) expected.Signon.Language = "ENG" expected.Signon.Org = "01" expected.Signon.Fid = "81729" var trnamt1, trnamt2, trnamt3 Amount trnamt1.SetFrac64(-796, 100) trnamt2.SetFrac64(383046, 100) trnamt3.SetFrac64(-1770, 100) banktranlist := TransactionList{ DtStart: *NewDate(2016, 12, 1, 15, 46, 48, 688000000, EST), DtEnd: *NewDate(2017, 3, 31, 15, 46, 48, 688000000, EDT), Transactions: []Transaction{ { TrnType: TrnTypeDebit, DtPosted: *NewDateGMT(2017, 2, 9, 12, 0, 0, 0), TrnAmt: trnamt1, FiTID: "2017020924435657040207171600195", Name: "SLICE OF NY", }, { TrnType: TrnTypeCredit, DtPosted: *NewDateGMT(2016, 12, 28, 12, 0, 0, 0), TrnAmt: trnamt2, FiTID: "2016122823633637200000258482730", Name: "Payment Thank You Electro", }, { TrnType: TrnTypeDebit, DtPosted: *NewDateGMT(2017, 3, 27, 12, 0, 0, 0), TrnAmt: trnamt3, FiTID: "2017032724445727085300442885680", Name: "KROGER FUEL #9999", }, }, } var balamt, availbalamt Amount balamt.SetFrac64(-933400, 100) availbalamt.SetFrac64(763017, 100) usd, err := NewCurrSymbol("USD") if err != nil { t.Fatalf("Unexpected error creating CurrSymbol for USD\n") } statementResponse := CCStatementResponse{ TrnUID: "59e850ad-7448-b4ce-4b71-29057763b306", Status: Status{ Code: 0, Severity: "INFO", }, CurDef: *usd, CCAcctFrom: CCAcct{ AcctID: "9283744488463775", }, BankTranList: &banktranlist, BalAmt: balamt, DtAsOf: *NewDate(2017, 3, 31, 8, 0, 0, 0, EDT), AvailBalAmt: &availbalamt, AvailDtAsOf: NewDate(2017, 3, 31, 8, 0, 0, 0, EDT), } expected.CreditCard = append(expected.CreditCard, &statementResponse) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } checkResponsesEqual(t, &expected, response) checkResponseRoundTrip(t, response) } ================================================ FILE: discovercard_client.go ================================================ 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) } ================================================ FILE: doc.go ================================================ /* Package ofxgo seeks to provide a library to make it easier to query and/or parse financial information with OFX from the comfort of Golang, without having to deal with marshalling/unmarshalling the 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 well. Instead, it exposes the OFX SGML/XML hierarchy as structs which mostly resemble it. For more information on OFX and to read the specification, see http://ofx.net. There are three main top-level objects defined in ofxgo. These are Client, Request, and Response. The Request and Response objects represent OFX requests and responses as Golang structs. Client contains settings which control how requests and responses are marshalled and unmarshalled (the OFX version used, client id and version, whether to indent SGML/XML tags, etc.), and provides helper methods for making requests and optionally parsing the response using those settings. Every Request object contains a SignonRequest element, called Signon. This element contains the username, password (or key), and the ORG and FID fields particular to the financial institution being queried, and an optional ClientUID field (required by some FIs). Likewise, each Response contains a SignonResponse object which contains, among other things, the Status of the request. Any status with a nonzero Code should be inspected for a possible error (using the Severity and Message fields populated by the server, or the CodeMeaning() and CodeConditions() functions which return information about a particular code as specified by the OFX specification). Each top-level Request or Response object may contain zero or more messages, sorted into named slices by message set, just as the OFX specification groups them. Here are the supported types of Request/Response objects (along with the name of the slice of Messages they belong to in parentheses): Requests: var r AcctInfoRequest // (Signup) Request a list of the valid accounts // for this user var r CCStatementRequest // (CreditCard) Request the balance (and optionally // list of transactions) for a credit card var r StatementRequest // (Bank) Request the balance (and optionally list // of transactions) for a bank account var r InvStatementRequest // (InvStmt) Request balance, transactions, // existing positions, and/or open orders for an // investment account var r SecListRequest // (SecList) Request securities details and prices var r ProfileRequest // (Prof) Request the server's capabilities (which // messages sets it supports, along with features) Responses: var r AcctInfoResponse // (Signup) List of the valid accounts for this // user var r CCStatementResponse // (CreditCard) The balance (and optionally list of // transactions) for a credit card var r StatementResponse // (Bank) The balance (and optionally list of // transactions) for a bank account var r InvStatementResponse // (InvStmt) The balance, transactions, existing // positions, and/or open orders for an // investment account var r SecListResponse // (SecList) Returned as a result of // SecListRequest, but only contains request // status var r SecurityList // (SecList) The actual list of securities, prices, // etc. (sent as a result of SecListRequest or // InvStatementRequest) var r ProfileResponse // (Prof) Describes the server's capabilities When constructing a Request, simply append the desired message to the message set it belongs to. For Responses, it is the user's responsibility to make type assertions on objects found inside one of these message sets before using them. For example, the following code would request a bank statement for a checking account and print the balance: import ( "fmt" "os" ) var client Client // By not initializing them, we accept all default // client values var request Request // These are all specific to you and your financial institution request.URL = "https://ofx.example.com" request.Signon.UserID = String("john") request.Signon.UserPass = String("hunter2") request.Signon.Org = String("MyBank") request.Signon.Fid = String("0001") uid, err := RandomUID() if err != nil { fmt.Println("Error creating uid for transaction:", err) os.Exit(1) } statementRequest := StatementRequest{ TrnUID: *uid, BankAcctFrom: BankAcct{ BankID: String("123456789"), AcctID: String("11111111111"), AcctType: AcctTypeChecking, }, } request.Bank = append(request.Bank, &statementRequest) response, err := client.Request(request) if err != nil { fmt.Println("Error requesting account statement:", err) os.Exit(1) } 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") } else if stmt, ok := response.Bank[0].(*StatementResponse); ok { fmt.Printf("Balance: %s %s (as of %s)\n", stmt.BalAmt, stmt.CurDef, stmt.DtAsOf) } More usage examples may be found in the example command-line client provided with this library, in the cmd/ofx directory of the source. */ package ofxgo ================================================ FILE: generate_constants.py ================================================ #!/usr/bin/env python enums = { # OFX spec version "OfxVersion": (["102", "103", "151", "160", "200", "201", "202", "203", "210", "211", "220"], "the OFX specification version in use"), # Bank/general "AcctType": (["Checking", "Savings", "MoneyMrkt", "CreditLine", "CD"], "types of bank accounts"), "TrnType": (["Credit", "Debit", "Int", "Div", "Fee", "SrvChg", "Dep", "ATM", "POS", "Xfer", "Check", "Payment", "Cash", "DirectDep", "DirectDebit", "RepeatPmt", "Hold", "Other"], "types of transactions. INT, ATM, and POS depend on the signage of the account."), "ImageType": (["Statement", "Transaction", "Tax"], "what this image contains"), "ImageRefType": (["Opaque", "URL", "FormURL"], "the type of reference to the image"), "CheckSup": (["FrontOnly", "BackOnly", "FrontAndBack"], "what portions of the check this image contains"), "CorrectAction": (["Delete", "Replace"], "whether this transaction correction replaces or deletes the transaction matching its CORRECTFITID"), "BalType": (["Dollar", "Percent", "Number"], "how this BAL's VALUE field should be interpreted"), # InvStmt "Inv401kSource": (["PreTax", "AfterTax", "Match", "ProfitSharing", "Rollover", "OtherVest", "OtherNonVest"], "the source of money used for this security in a 401(k) account. Default if not present is OTHERNONVEST. The following cash source types are subject to vesting: MATCH, PROFITSHARING, and OTHERVEST."), "SubAcctType": (["Cash", "Margin", "Short", "Other"], "the sub-account type for a source and/or destination of a transaction. Used in fields named SubAcctFrom, SubAcctTo, SubAcctSec, SubAcctFund, HeldInAcct."), "BuyType": (["Buy", "BuyToCover"], "types of purchases"), "OptAction": (["Exercise", "Assign", "Expire"], "types of actions for options"), "TferAction": (["In", "Out"], "whether the transfer is into or out of this account"), "PosType": (["Long", "Short"], "position type"), "Secured": (["Naked", "Covered"], "how an option is secured"), "Duration": (["Day", "GoodTilCancel", "Immediate"], "how long the investment order is good for"), "Restriction": (["AllOrNone", "MinUnits", "None"], "a special restriction on an investment order"), "UnitType": (["Shares", "Currency"], "type of the UNITS value"), "OptBuyType": (["BuyToOpen", "BuyToClose"], "types of purchases for options"), "SellType": (["Sell", "SellShort"], "types of sales"), "LoanPmtFreq": (["Weekly", "Biweekly", "TwiceMonthly", "Monthly", "FourWeeks", "BiMonthly", "Quarterly", "Semiannually", "Annually", "Other"], "the frequency of loan payments"), "IncomeType": (["CGLong", "CGShort", "Div", "Interest", "Misc"], "types of investment income"), "SellReason": (["Call", "Sell", "Maturity"], "the reason the sell of a debt security was generated: CALL (the debt was called), SELL (the debt was sold), MATURITY (the debt reached maturity)"), "OptSellType": (["SellToClose", "SellToOpen"], "types of sales for options"), "RelType": (["Spread", "Straddle", "None", "Other"], "related option transaction types"), # Prof "CharType": (["AlphaOnly", "NumericOnly", "AlphaOrNumeric", "AlphaAndNumeric"], "types of characters allowed in password"), "SyncMode": (["Full", "Lite"], "data synchronization mode supported (see OFX spec for more details)"), "OfxSec": (["None", "Type 1"], "the type of application-level security required for the message set"), # SecList "DebtType": (["Coupon", "Zero"], "debt type"), "DebtClass": (["Treasury", "Municipal", "Corporate", "Other"], "the class of debt"), "CouponFreq": (["Monthly", "Quarterly", "Semiannual", "Annual", "Other"], "when debt coupons mature"), "CallType": (["Call", "Put", "Prefund", "Maturity"], "type of next call (for a debt)"), "AssetClass": (["DomesticBond", "IntlBond", "LargeStock", "SmallStock", "IntlStock", "MoneyMrkt", "Other"], "type of asset classes"), "MfType": (["OpenEnd", "CloseEnd", "Other"], "types of mutual funds"), "OptType": (["Put", "Call"], "whether the option is a PUT or a CALL"), "StockType": (["Common", "Preferred", "Convertible", "Other"], "types of stock"), # Signup "HolderType": (["Individual", "Joint", "Custodial", "Trust", "Other"], "how the account is held"), "AcctClassification": (["Personal", "Business", "Corporate", "Other"], "the type of an account"), "SvcStatus": (["Avail", "Pend", "Active"], "the status of the account: AVAIL = Available, but not yet requested, PEND = Requested, but not yet available, ACTIVE = In use"), "UsProductType": (["401K", "403B", "IRA", "KEOGH", "Other", "SARSEP", "Simple", "Normal", "TDA", "Trust", "UGMA"], "type of investment account (in the US)"), } header = """package ofxgo /* * Do not edit this file by hand. It is auto-generated by calling `go generate`. * To make changes, edit generate_constants.py, re-run `go generate`, and check * in the result. */ import ( "errors" "fmt" "strings" "github.com/aclindsa/xml" ) """ template = """ type {enumLower} uint // {enum}* constants represent {comment} const ( {constNames}) var {enumLower}s = [...]string{{"{upperValueString}"}} func (e {enumLower}) Valid() bool {{ // This check is mostly out of paranoia, ensuring e != 0 should be // sufficient return e >= {firstValue} && e <= {lastValue} }} func (e {enumLower}) String() string {{ if e.Valid() {{ return {enumLower}s[e-1] }} return fmt.Sprintf("invalid {enumLower} (%d)", e) }} func (e *{enumLower}) FromString(in string) error {{ value := strings.TrimSpace(in) for i, s := range {enumLower}s {{ if s == value {{ *e = {enumLower}(i + 1) return nil }} }} *e = 0 return errors.New("Invalid {enum}: \\\"" + in + "\\\"") }} func (e *{enumLower}) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {{ var value string err := d.DecodeElement(&value, &start) if err != nil {{ return err }} return e.FromString(value) }} func (e {enumLower}) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {{ if !e.Valid() {{ return nil }} enc.EncodeElement({enumLower}s[e-1], start) return nil }} // New{enum} returns returns an 'enum' value of type {enumLower} given its // string representation func New{enum}(s string) ({enumLower}, error) {{ var e {enumLower} err := e.FromString(s) if err != nil {{ return 0, err }} return e, nil }} """ with open("constants.go", 'w') as f: f.write(header) for enum in enums: enumLower = enum[:1].lower() + enum[1:].replace(" ", "") firstValue = enum+enums[enum][0][0].replace(" ", "") lastValue = enum+enums[enum][0][-1].replace(" ", "") comment = enums[enum][1] constNames = "\t{firstValue} {enumLower} = 1 + iota\n".format( enum=enum, firstValue=firstValue, enumLower=enumLower) for value in enums[enum][0][1:]: constNames += "\t{enum}{value}\n".format( enum=enum, value=value.replace(" ", "")) upperValueString = "\", \"".join([s.upper() for s in enums[enum][0]]) f.write(template.format(enum=enum, enumLower=enumLower, comment=comment, firstValue=firstValue, lastValue=lastValue, constNames=constNames, upperValueString=upperValueString)) test_header = """package ofxgo /* * Do not edit this file by hand. It is auto-generated by calling `go generate`. * To make changes, edit generate_constants.py, re-run `go generate`, and check * in the result. */ import ( "strings" "testing" "github.com/aclindsa/xml" ) """ test_template = """ func Test{enum}(t *testing.T) {{ e, err := New{enum}("{firstValueUpper}") if err != nil {{ t.Fatalf("Unexpected error creating new {enum} from string \\\"{firstValueUpper}\\\"\\n") }} if !e.Valid() {{ t.Fatalf("{enum} unexpectedly invalid\\n") }} err = e.FromString("{lastValueUpper}") if err != nil {{ t.Fatalf("Unexpected error on {enum}.FromString(\\\"{lastValueUpper}\\\")\\n") }} if e.String() != "{lastValueUpper}" {{ t.Fatalf("{enum}.String() expected to be \\\"{lastValueUpper}\\\"\\n") }} marshalHelper(t, "{lastValueUpper}", &e) overwritten, err := New{enum}("THISWILLNEVERBEAVALIDENUMSTRING") if err == nil {{ t.Fatalf("Expected error creating new {enum} from string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\"\\n") }} if overwritten.Valid() {{ t.Fatalf("{enum} created with string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\" should not be valid\\n") }} if !strings.Contains(strings.ToLower(overwritten.String()), "invalid") {{ t.Fatalf("{enum} created with string \\\"THISWILLNEVERBEAVALIDENUMSTRING\\\" should not return valid string from String()\\n") }} b, err := xml.Marshal(&overwritten) if err != nil {{ t.Fatalf("Unexpected error on xml.Marshal({enum}): %s\\n", err) }} if string(b) != "" {{ t.Fatalf("Expected empty string, got '%s'\\n", string(b)) }} unmarshalHelper(t, "{lastValueUpper}", &e, &overwritten) err = xml.Unmarshal([]byte(""), &overwritten) if err == nil {{ t.Fatalf("Expected error unmarshalling garbage value\\n") }} type SC struct {{ E {enumLower} }} sc := SC{{E: e}} b, err = xml.Marshal(sc) if err != nil {{ t.Fatalf("Unexpected error on xml.Marshal(struct {enum}): %s\\n", err) }} if string(b) != "{lastValueUpper}" {{ t.Fatalf("Expected '%s', got '%s'\\n", "{lastValueUpper}", string(b)) }} }} """ with open("constants_test.go", 'w') as f: f.write(test_header) for enum in enums: enumLower = enum[:1].lower() + enum[1:].replace(" ", "") firstValueUpper = enums[enum][0][0].upper() lastValueUpper = enums[enum][0][-1].upper() f.write(test_template.format(enum=enum, enumLower=enumLower, firstValueUpper=firstValueUpper, lastValueUpper=lastValueUpper)) ================================================ FILE: go.mod ================================================ 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.8 ) go 1.9 ================================================ FILE: go.sum ================================================ 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= ================================================ FILE: invstmt.go ================================================ package ofxgo import ( "errors" "github.com/aclindsa/xml" ) // InvStatementRequest allows a customer to request transactions, positions, // open orders, and balances. It specifies what types of information to include // in hte InvStatementResponse and which account to include it for. type InvStatementRequest struct { XMLName xml.Name `xml:"INVSTMTTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` InvAcctFrom InvAcct `xml:"INVSTMTRQ>INVACCTFROM"` DtStart *Date `xml:"INVSTMTRQ>INCTRAN>DTSTART,omitempty"` DtEnd *Date `xml:"INVSTMTRQ>INCTRAN>DTEND,omitempty"` Include Boolean `xml:"INVSTMTRQ>INCTRAN>INCLUDE"` // Include transactions (instead of just balance) IncludeOO Boolean `xml:"INVSTMTRQ>INCOO"` // Include open orders PosDtAsOf *Date `xml:"INVSTMTRQ>INCPOS>DTASOF,omitempty"` // Date that positions should be sent down for, if present IncludePos Boolean `xml:"INVSTMTRQ>INCPOS>INCLUDE"` // Include position data in response IncludeBalance Boolean `xml:"INVSTMTRQ>INCBAL"` // Include investment balance in response Include401K Boolean `xml:"INVSTMTRQ>INC401K,omitempty"` // Include 401k information Include401KBal Boolean `xml:"INVSTMTRQ>INC401KBAL,omitempty"` // Include 401k balance information IncludeTranImage Boolean `xml:"INVSTMTRQ>INCTRANIMAGE,omitempty"` // Include transaction images } // Name returns the name of the top-level transaction XML/SGML element func (r *InvStatementRequest) Name() string { return "INVSTMTTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *InvStatementRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement return true, nil } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *InvStatementRequest) Type() messageType { return InvStmtRq } // InvTran represents generic investment transaction. It is included in both // InvBuy and InvSell as well as many of the more specific transaction // aggregates. type InvTran struct { XMLName xml.Name `xml:"INVTRAN"` FiTID String `xml:"FITID"` // Unique FI-assigned transaction ID. This ID is used to detect duplicate downloads SrvrTID String `xml:"SRVRTID,omitempty"` // Server assigned transaction ID DtTrade Date `xml:"DTTRADE"` // trade date; for stock splits, day of record DtSettle *Date `xml:"DTSETTLE,omitempty"` // settlement date; for stock splits, execution date ReversalFiTID String `xml:"REVERSALFITID,omitempty"` // For a reversal transaction, the FITID of the transaction that is being reversed. Memo String `xml:"MEMO,omitempty"` } // InvBuy represents generic investment purchase transaction. It is included // in many of the more specific transaction Buy* aggregates below. type InvBuy struct { XMLName xml.Name `xml:"INVBUY"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts UnitPrice Amount `xml:"UNITPRICE"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security Markup Amount `xml:"MARKUP,omitempty"` // Portion of UNITPRICE that is attributed to the dealer markup Commission Amount `xml:"COMMISSION,omitempty"` Taxes Amount `xml:"TAXES,omitempty"` Fees Amount `xml:"FEES,omitempty"` Load Amount `xml:"LOAD,omitempty"` 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 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 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 // The next three elements must either all be provided, or none of them 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 LoanPrincipal Amount `xml:"LOANPRINCIPAL,omitempty"` // For 401(k) accounts only. Indicates how much of the loan repayment was principal LoanInterest Amount `xml:"LOANINTEREST,omitempty"` // For 401(k) accounts only. Indicates how much of the loan repayment was interest 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 DtPayroll *Date `xml:"DTPAYROLL,omitempty"` // For 401(k)accounts, date the funds for this transaction was obtained via payroll deduction PriorYearContrib Boolean `xml:"PRIORYEARCONTRIB,omitempty"` // For 401(k) accounts, indicates that this Buy was made with a prior year contribution } // InvSell represents generic investment sale transaction. It is included in // many of the more specific transaction Sell* aggregates below. type InvSell struct { XMLName xml.Name `xml:"INVSELL"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts UnitPrice Amount `xml:"UNITPRICE"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security Markdown Amount `xml:"MARKDOWN,omitempty"` // Portion of UNITPRICE that is attributed to the dealer markdown Commission Amount `xml:"COMMISSION,omitempty"` Taxes Amount `xml:"TAXES,omitempty"` Fees Amount `xml:"FEES,omitempty"` Load Amount `xml:"LOAD,omitempty"` Withholding Amount `xml:"WITHHOLDING,omitempty"` // Federal tax withholdings 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 Gain Amount `xml:"GAIN,omitempty"` // Total gain 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 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 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 StateWithholding Amount `xml:"STATEWITHHOLDING,omitempty"` // State tax withholdings 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 } // BuyDebt represents a transaction purchasing a debt security type BuyDebt struct { XMLName xml.Name `xml:"BUYDEBT"` InvBuy InvBuy `xml:"INVBUY"` AccrdInt Amount `xml:"ACCRDINT,omitempty"` // Accrued interest. This amount is not reflected in the field of a containing aggregate. } // TransactionType returns a string representation of this transaction's type func (t BuyDebt) TransactionType() string { return "BUYDEBT" } func (t BuyDebt) InvTransaction() InvTran { return t.InvBuy.InvTran } // BuyMF represents a transaction purchasing a mutual fund type BuyMF struct { XMLName xml.Name `xml:"BUYMF"` InvBuy InvBuy `xml:"INVBUY"` BuyType buyType `xml:"BUYTYPE"` // One of BUY, BUYTOCOVER (BUYTOCOVER used to close short sales.) RelFiTID String `xml:"RELFITID,omitempty"` // used to relate transactions associated with mutual fund exchanges } // TransactionType returns a string representation of this transaction's type func (t BuyMF) TransactionType() string { return "BUYMF" } func (t BuyMF) InvTransaction() InvTran { return t.InvBuy.InvTran } // BuyOpt represents a transaction purchasing an option type BuyOpt struct { XMLName xml.Name `xml:"BUYOPT"` InvBuy InvBuy `xml:"INVBUY"` OptBuyType optBuyType `xml:"OPTBUYTYPE"` // type of purchase: BUYTOOPEN, BUYTOCLOSE (The BUYTOOPEN buy type is like “ordinary” buying of option and works like stocks.) ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract } // TransactionType returns a string representation of this transaction's type func (t BuyOpt) TransactionType() string { return "BUYOPT" } func (t BuyOpt) InvTransaction() InvTran { return t.InvBuy.InvTran } // BuyOther represents a transaction purchasing a type of security not covered // by the other Buy* structs type BuyOther struct { XMLName xml.Name `xml:"BUYOTHER"` InvBuy InvBuy `xml:"INVBUY"` } // TransactionType returns a string representation of this transaction's type func (t BuyOther) TransactionType() string { return "BUYOTHER" } func (t BuyOther) InvTransaction() InvTran { return t.InvBuy.InvTran } // BuyStock represents a transaction purchasing stock type BuyStock struct { XMLName xml.Name `xml:"BUYSTOCK"` InvBuy InvBuy `xml:"INVBUY"` BuyType buyType `xml:"BUYTYPE"` // One of BUY, BUYTOCOVER (BUYTOCOVER used to close short sales.) } // TransactionType returns a string representation of this transaction's type func (t BuyStock) TransactionType() string { return "BUYSTOCK" } func (t BuyStock) InvTransaction() InvTran { return t.InvBuy.InvTran } // ClosureOpt represents a transaction closing a position for an option type ClosureOpt struct { XMLName xml.Name `xml:"CLOSUREOPT"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` OptAction optAction `xml:"OPTACTION"` // One of EXERCISE, ASSIGN, EXPIRE. The EXERCISE action is used to close out an option that is exercised. The ASSIGN action is used when an option writer is assigned. The EXPIRE action is used when the option’s expired date is reached Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER RelFiTID String `xml:"RELFITID,omitempty"` // used to relate transactions associated with mutual fund exchanges Gain Amount `xml:"GAIN,omitempty"` // Total gain } // TransactionType returns a string representation of this transaction's type func (t ClosureOpt) TransactionType() string { return "CLOSUREOPT" } func (t ClosureOpt) InvTransaction() InvTran { return t.InvTran } // Income represents a transaction where investment income is being realized as // cash into the investment account type Income struct { XMLName xml.Name `xml:"INCOME"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` IncomeType incomeType `xml:"INCOMETYPE"` // Type of investment income: CGLONG (capital gains-long term), CGSHORT (capital gains-short term), DIV (dividend), INTEREST, MISC Total Amount `xml:"TOTAL"` 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 TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction 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() 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 } // TransactionType returns a string representation of this transaction's type func (t Income) TransactionType() string { return "INCOME" } func (t Income) InvTransaction() InvTran { return t.InvTran } // InvExpense represents a transaction realizing an expense associated with an // investment type InvExpense struct { XMLName xml.Name `xml:"INVEXPENSE"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` Total Amount `xml:"TOTAL"` 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 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 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 } // TransactionType returns a string representation of this transaction's type func (t InvExpense) TransactionType() string { return "INVEXPENSE" } func (t InvExpense) InvTransaction() InvTran { return t.InvTran } // JrnlFund represents a transaction journaling cash holdings between // sub-accounts within the same investment account type JrnlFund struct { XMLName xml.Name `xml:"JRNLFUND"` InvTran InvTran `xml:"INVTRAN"` Total Amount `xml:"TOTAL"` SubAcctFrom subAcctType `xml:"SUBACCTFROM"` // Sub-account cash is being transferred from: CASH, MARGIN, SHORT, OTHER SubAcctTo subAcctType `xml:"SUBACCTTO"` // Sub-account cash is being transferred to: CASH, MARGIN, SHORT, OTHER } // TransactionType returns a string representation of this transaction's type func (t JrnlFund) TransactionType() string { return "JRNLFUND" } func (t JrnlFund) InvTransaction() InvTran { return t.InvTran } // JrnlSec represents a transaction journaling security holdings between // sub-accounts within the same investment account type JrnlSec struct { XMLName xml.Name `xml:"JRNLSEC"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` SubAcctFrom subAcctType `xml:"SUBACCTFROM"` // Sub-account cash is being transferred from: CASH, MARGIN, SHORT, OTHER SubAcctTo subAcctType `xml:"SUBACCTTO"` // Sub-account cash is being transferred to: CASH, MARGIN, SHORT, OTHER Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts } // TransactionType returns a string representation of this transaction's type func (t JrnlSec) TransactionType() string { return "JRNLSEC" } func (t JrnlSec) InvTransaction() InvTran { return t.InvTran } // MarginInterest represents a transaction realizing a margin interest expense type MarginInterest struct { XMLName xml.Name `xml:"MARGININTEREST"` InvTran InvTran `xml:"INVTRAN"` Total Amount `xml:"TOTAL"` SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER 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 } // TransactionType returns a string representation of this transaction's type func (t MarginInterest) TransactionType() string { return "MARGININTEREST" } func (t MarginInterest) InvTransaction() InvTran { return t.InvTran } // Reinvest is a single transaction that contains both income and an investment // transaction. If servers can’t track this as a single transaction they should // return an Income transaction and an InvTran. type Reinvest struct { XMLName xml.Name `xml:"REINVEST"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` IncomeType incomeType `xml:"INCOMETYPE"` // Type of investment income: CGLONG (capital gains-long term), CGSHORT (capital gains-short term), DIV (dividend), INTEREST, MISC 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 SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts UnitPrice Amount `xml:"UNITPRICE"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security Commission Amount `xml:"COMMISSION,omitempty"` Taxes Amount `xml:"TAXES,omitempty"` Fees Amount `xml:"FEES,omitempty"` Load Amount `xml:"LOAD,omitempty"` TaxExempt Boolean `xml:"TAXEXEMPT,omitempty"` // Tax-exempt transaction 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 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 } // TransactionType returns a string representation of this transaction's type func (t Reinvest) TransactionType() string { return "REINVEST" } func (t Reinvest) InvTransaction() InvTran { return t.InvTran } // RetOfCap represents a transaction where capital is being returned to the // account holder type RetOfCap struct { XMLName xml.Name `xml:"RETOFCAP"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` Total Amount `xml:"TOTAL"` 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 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 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 } // TransactionType returns a string representation of this transaction's type func (t RetOfCap) TransactionType() string { return "RETOFCAP" } func (t RetOfCap) InvTransaction() InvTran { return t.InvTran } // SellDebt represents the sale of a debt security. Used when debt is sold, // called, or reaches maturity. type SellDebt struct { XMLName xml.Name `xml:"SELLDEBT"` InvSell InvSell `xml:"INVSELL"` SellReason sellReason `xml:"SELLREASON"` // CALL (the debt was called), SELL (the debt was sold), MATURITY (the debt reached maturity) AccrdInt Amount `xml:"ACCRDINT,omitempty"` // Accrued interest } // TransactionType returns a string representation of this transaction's type func (t SellDebt) TransactionType() string { return "SELLDEBT" } func (t SellDebt) InvTransaction() InvTran { return t.InvSell.InvTran } // SellMF represents a transaction selling a mutual fund type SellMF struct { XMLName xml.Name `xml:"SELLMF"` InvSell InvSell `xml:"INVSELL"` SellType sellType `xml:"SELLTYPE"` // Type of sell. SELL, SELLSHORT AvgCostBasis Amount `xml:"AVGCOSTBASIS"` RelFiTID String `xml:"RELFITID,omitempty"` // used to relate transactions associated with mutual fund exchanges } // TransactionType returns a string representation of this transaction's type func (t SellMF) TransactionType() string { return "SELLMF" } func (t SellMF) InvTransaction() InvTran { return t.InvSell.InvTran } // SellOpt represents a transaction selling an option. Depending on the value // of OptSellType, can be used to sell a previously bought option or write a // new option. type SellOpt struct { XMLName xml.Name `xml:"SELLOPT"` InvSell InvSell `xml:"INVSELL"` OptSellType optSellType `xml:"OPTSELLTYPE"` // For options, type of sell: SELLTOCLOSE, SELLTOOPEN. The SELLTOCLOSE action is selling a previously bought option. The SELLTOOPEN action is writing an option ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract RelFiTID String `xml:"RELFITID,omitempty"` // used to relate transactions associated with mutual fund exchanges RelType relType `xml:"RELTYPE,omitempty"` // Related option transaction type: SPREAD, STRADDLE, NONE, OTHER Secured secured `xml:"SECURED,omitempty"` // NAKED, COVERED } // TransactionType returns a string representation of this transaction's type func (t SellOpt) TransactionType() string { return "SELLOPT" } func (t SellOpt) InvTransaction() InvTran { return t.InvSell.InvTran } // SellOther represents a transaction selling a security type not covered by // the other Sell* structs type SellOther struct { XMLName xml.Name `xml:"SELLOTHER"` InvSell InvSell `xml:"INVSELL"` } // TransactionType returns a string representation of this transaction's type func (t SellOther) TransactionType() string { return "SELLOTHER" } func (t SellOther) InvTransaction() InvTran { return t.InvSell.InvTran } // SellStock represents a transaction selling stock type SellStock struct { XMLName xml.Name `xml:"SELLSTOCK"` InvSell InvSell `xml:"INVSELL"` SellType sellType `xml:"SELLTYPE"` // Type of sell. SELL, SELLSHORT } // TransactionType returns a string representation of this transaction's type func (t SellStock) TransactionType() string { return "SELLSTOCK" } func (t SellStock) InvTransaction() InvTran { return t.InvSell.InvTran } // Split represents a stock or mutual fund split type Split struct { XMLName xml.Name `xml:"SPLIT"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER OldUnits Amount `xml:"OLDUNITS"` // number of shares before the split NewUnits Amount `xml:"NEWUNITS"` // number of shares after the split Numerator Int `xml:"NUMERATOR"` // split ratio numerator Denominator Int `xml:"DENOMINATOR"` // split ratio denominator 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 FracCash Amount `xml:"FRACCASH,omitempty"` // cash for fractional units SubAcctFund subAcctType `xml:"SUBACCTFUND,omitempty"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER 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 } // TransactionType returns a string representation of this transaction's type func (t Split) TransactionType() string { return "SPLIT" } func (t Split) InvTransaction() InvTran { return t.InvTran } // Transfer represents the transfer of securities into or out of an account type Transfer struct { XMLName xml.Name `xml:"TRANSFER"` InvTran InvTran `xml:"INVTRAN"` SecID SecurityID `xml:"SECID"` SubAcctSec subAcctType `xml:"SUBACCTSEC"` // Sub-account type for this security. One of CASH, MARGIN, SHORT, OTHER Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts TferAction tferAction `xml:"TFERACTION"` // One of IN, OUT PosType posType `xml:"POSTYPE"` // Position type. One of LONG, SHORT InvAcctFrom InvAcct `xml:"INVACCTFROM,omitempty"` AvgCostBasis Amount `xml:"AVGCOSTBASIS,omitempty"` UnitPrice Amount `xml:"UNITPRICE,omitempty"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security DtPurchase *Date `xml:"DTPURCHASE,omitempty"` 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 } // TransactionType returns a string representation of this transaction's type func (t Transfer) TransactionType() string { return "TRANSFER" } func (t Transfer) InvTransaction() InvTran { return t.InvTran } // InvTransaction is a generic interface met by all investment transactions // (Buy*, Sell*, & co.) type InvTransaction interface { TransactionType() string InvTransaction() InvTran } // InvBankTransaction is a banking transaction performed in an investment // account. This represents all transactions not related to securities - for // instance, funding the account using cash from another bank. type InvBankTransaction struct { XMLName xml.Name `xml:"INVBANKTRAN"` Transactions []Transaction `xml:"STMTTRN,omitempty"` SubAcctFund subAcctType `xml:"SUBACCTFUND"` // Where did the money for the transaction come from or go to? CASH, MARGIN, SHORT, OTHER } // InvTranList represents a list of investment account transactions. It // includes the date range its transactions cover, as well as the bank- and // security-related transactions themselves. It must be unmarshalled manually // due to the structure (don't know what kind of InvTransaction is coming next) type InvTranList struct { XMLName xml.Name `xml:"INVTRANLIST"` DtStart Date DtEnd Date // This is the value that should be sent as in the next InvStatementRequest to ensure that no transactions are missed InvTransactions []InvTransaction BankTransactions []InvBankTransaction } // UnmarshalXML handles unmarshalling an InvTranList element from an SGML/XML // string func (l *InvTranList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if startElement, ok := tok.(xml.StartElement); ok { switch startElement.Name.Local { case "DTSTART": var dtstart Date if err := d.DecodeElement(&dtstart, &startElement); err != nil { return err } l.DtStart = dtstart case "DTEND": var dtend Date if err := d.DecodeElement(&dtend, &startElement); err != nil { return err } l.DtEnd = dtend case "BUYDEBT": var tran BuyDebt if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "BUYMF": var tran BuyMF if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "BUYOPT": var tran BuyOpt if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "BUYOTHER": var tran BuyOther if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "BUYSTOCK": var tran BuyStock if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "CLOSUREOPT": var tran ClosureOpt if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "INCOME": var tran Income if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "INVEXPENSE": var tran InvExpense if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "JRNLFUND": var tran JrnlFund if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "JRNLSEC": var tran JrnlSec if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "MARGININTEREST": var tran MarginInterest if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "REINVEST": var tran Reinvest if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "RETOFCAP": var tran RetOfCap if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SELLDEBT": var tran SellDebt if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SELLMF": var tran SellMF if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SELLOPT": var tran SellOpt if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SELLOTHER": var tran SellOther if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SELLSTOCK": var tran SellStock if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "SPLIT": var tran Split if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "TRANSFER": var tran Transfer if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.InvTransactions = append(l.InvTransactions, InvTransaction(tran)) case "INVBANKTRAN": var tran InvBankTransaction if err := d.DecodeElement(&tran, &startElement); err != nil { return err } l.BankTransactions = append(l.BankTransactions, tran) default: return errors.New("Invalid INVTRANLIST child tag: " + startElement.Name.Local) } } else { return errors.New("Didn't find an opening element") } } } // 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 // other *Position types type InvPosition struct { XMLName xml.Name `xml:"INVPOS"` SecID SecurityID `xml:"SECID"` HeldInAcct subAcctType `xml:"HELDINACCT"` // Sub-account type, one of CASH, MARGIN, SHORT, OTHER PosType posType `xml:"POSTYPE"` // SHORT = Writer for options, Short for all others; LONG = Holder for options, Long for all others. Units Amount `xml:"UNITS"` // For stocks, MFs, other, number of shares held. Bonds = face value. Options = number of contracts UnitPrice Amount `xml:"UNITPRICE"` // For stocks, MFs, other, price per share. Bonds = percentage of par. Option = premium per share of underlying security MktVal Amount `xml:"MKTVAL"` // Market value of this position AvgCostBasis Amount `xml:"AVGCOSTBASIS,omitempty"` // DtPriceAsOf Date `xml:"DTPRICEASOF"` // Date and time of unit price and market value, and cost basis. If this date is unknown, use 19900101 as the placeholder; do not use 0, Currency *Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE Memo String `xml:"MEMO,omitempty"` Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // 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 } // Position is an interface satisfied by all the other *Position types type Position interface { PositionType() string InvPosition() InvPosition } // DebtPosition represents a position held in a debt security type DebtPosition struct { XMLName xml.Name `xml:"POSDEBT"` InvPos InvPosition `xml:"INVPOS"` } // PositionType returns a string representation of this position's type func (p DebtPosition) PositionType() string { return "POSDEBT" } // InvPosition returns InvPos func (p DebtPosition) InvPosition() InvPosition { return p.InvPos } // MFPosition represents a position held in a mutual fund type MFPosition struct { XMLName xml.Name `xml:"POSMF"` InvPos InvPosition `xml:"INVPOS"` UnitsStreet Amount `xml:"UNITSSTREET,omitempty"` // Units in the FI’s street name UnitsUser Amount `xml:"UNITSUSER,omitempty"` // Units in the user's name directly ReinvDiv Boolean `xml:"REINVDIV,omitempty"` // Reinvest dividends ReinvCG Boolean `xml:"REINVCG,omitempty"` // Reinvest capital gains } // PositionType returns a string representation of this position's type func (p MFPosition) PositionType() string { return "POSMF" } // InvPosition returns InvPos func (p MFPosition) InvPosition() InvPosition { return p.InvPos } // OptPosition represents a position held in an option type OptPosition struct { XMLName xml.Name `xml:"POSOPT"` InvPos InvPosition `xml:"INVPOS"` Secured secured `xml:"SECURED,omitempty"` // One of NAKED, COVERED } // PositionType returns a string representation of this position's type func (p OptPosition) PositionType() string { return "POSOPT" } // InvPosition returns InvPos func (p OptPosition) InvPosition() InvPosition { return p.InvPos } // OtherPosition represents a position held in a security type not covered by // the other *Position elements type OtherPosition struct { XMLName xml.Name `xml:"POSOTHER"` InvPos InvPosition `xml:"INVPOS"` } // PositionType returns a string representation of this position's type func (p OtherPosition) PositionType() string { return "POSOTHER" } // InvPosition returns InvPos func (p OtherPosition) InvPosition() InvPosition { return p.InvPos } // StockPosition represents a position held in a stock type StockPosition struct { XMLName xml.Name `xml:"POSSTOCK"` InvPos InvPosition `xml:"INVPOS"` UnitsStreet Amount `xml:"UNITSSTREET,omitempty"` // Units in the FI’s street name UnitsUser Amount `xml:"UNITSUSER,omitempty"` // Units in the user's name directly ReinvDiv Boolean `xml:"REINVDIV,omitempty"` // Reinvest dividends } // PositionType returns a string representation of this position's type func (p StockPosition) PositionType() string { return "POSSTOCK" } // InvPosition returns InvPos func (p StockPosition) InvPosition() InvPosition { return p.InvPos } // PositionList represents a list of positions held in securities in an // investment account type PositionList []Position // UnmarshalXML handles unmarshalling a PositionList from an XML string func (p *PositionList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if startElement, ok := tok.(xml.StartElement); ok { switch startElement.Name.Local { case "POSDEBT": var position DebtPosition if err := d.DecodeElement(&position, &startElement); err != nil { return err } *p = append(*p, Position(position)) case "POSMF": var position MFPosition if err := d.DecodeElement(&position, &startElement); err != nil { return err } *p = append(*p, Position(position)) case "POSOPT": var position OptPosition if err := d.DecodeElement(&position, &startElement); err != nil { return err } *p = append(*p, Position(position)) case "POSOTHER": var position OtherPosition if err := d.DecodeElement(&position, &startElement); err != nil { return err } *p = append(*p, Position(position)) case "POSSTOCK": var position StockPosition if err := d.DecodeElement(&position, &startElement); err != nil { return err } *p = append(*p, Position(position)) default: return errors.New("Invalid INVPOSLIST child tag: " + startElement.Name.Local) } } else { return errors.New("Didn't find an opening element") } } } // 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 // a free-form list of generic balance information which may be provided by an // FI. type InvBalance struct { XMLName xml.Name `xml:"INVBAL"` AvailCash Amount `xml:"AVAILCASH"` // Available cash across all sub-accounts, including sweep funds MarginBalance Amount `xml:"MARGINBALANCE"` // Negative means customer has borrowed funds ShortBalance Amount `xml:"SHORTBALANCE"` // Always positive, market value of all short positions BuyPower Amount `xml:"BUYPOWER,omitempty"` BalList []Balance `xml:"BALLIST>BAL,omitempty"` } // OO represents a generic open investment order. It is included in the other // OO* elements. type OO struct { XMLName xml.Name `xml:"OO"` FiTID String `xml:"FITID"` SrvrTID String `xml:"SRVRTID,omitempty"` SecID SecurityID `xml:"SECID"` DtPlaced Date `xml:"DTPLACED"` // Date the order was placed Units Amount `xml:"UNITS"` // Quantity of the security the open order is for SubAcct subAcctType `xml:"SUBACCT"` // One of CASH, MARGIN, SHORT, OTHER Duration duration `xml:"DURATION"` // How long the order is good for. One of DAY, GOODTILCANCEL, IMMEDIATE Restriction restriction `xml:"RESTRICTION"` // Special restriction on the order: One of ALLORNONE, MINUNITS, NONE MinUnits Amount `xml:"MINUNITS,omitempty"` // Minimum number of units that must be filled for the order LimitPrice Amount `xml:"LIMITPRICE,omitempty"` StopPrice Amount `xml:"STOPPRICE,omitempty"` Memo String `xml:"MEMO,omitempty"` Currency *Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE Inv401kSource inv401kSource `xml:"INV401KSOURCE,omitempty"` // 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 } // OpenOrder is an interface satisfied by all the OO* elements. type OpenOrder interface { OrderType() string } // OOBuyDebt represents an open order to purchase a debt security type OOBuyDebt struct { XMLName xml.Name `xml:"OOBUYDEBT"` OO OO `xml:"OO"` Auction Boolean `xml:"AUCTION"` // whether the debt should be purchased at the auction DtAuction *Date `xml:"DTAUCTION,omitempty"` } // OrderType returns a string representation of this order's type func (o OOBuyDebt) OrderType() string { return "OOBUYDEBT" } // OOBuyMF represents an open order to purchase a mutual fund type OOBuyMF struct { XMLName xml.Name `xml:"OOBUYMF"` OO OO `xml:"OO"` BuyType buyType `xml:"BUYTYPE"` // One of BUY, BUYTOCOVER UnitType unitType `xml:"UNITTYPE"` // What the units represent: one of SHARES, CURRENCY } // OrderType returns a string representation of this order's type func (o OOBuyMF) OrderType() string { return "OOBUYMF" } // OOBuyOpt represents an open order to purchase an option type OOBuyOpt struct { XMLName xml.Name `xml:"OOBUYOPT"` OO OO `xml:"OO"` OptBuyType optBuyType `xml:"OPTBUYTYPE"` // One of BUYTOOPEN, BUYTOCLOSE } // OrderType returns a string representation of this order's type func (o OOBuyOpt) OrderType() string { return "OOBUYOPT" } // OOBuyOther represents an open order to purchase a security type not covered // by the other OOBuy* elements type OOBuyOther struct { XMLName xml.Name `xml:"OOBUYOTHER"` OO OO `xml:"OO"` UnitType unitType `xml:"UNITTYPE"` // What the units represent: one of SHARES, CURRENCY } // OrderType returns a string representation of this order's type func (o OOBuyOther) OrderType() string { return "OOBUYOTHER" } // OOBuyStock represents an open order to purchase stock type OOBuyStock struct { XMLName xml.Name `xml:"OOBUYSTOCK"` OO OO `xml:"OO"` BuyType buyType `xml:"BUYTYPE"` // One of BUY, BUYTOCOVER } // OrderType returns a string representation of this order's type func (o OOBuyStock) OrderType() string { return "OOBUYSTOCK" } // OOSellDebt represents an open order to sell a debt security type OOSellDebt struct { XMLName xml.Name `xml:"OOSELLDEBT"` OO OO `xml:"OO"` } // OrderType returns a string representation of this order's type func (o OOSellDebt) OrderType() string { return "OOSELLDEBT" } // OOSellMF represents an open order to sell a mutual fund type OOSellMF struct { XMLName xml.Name `xml:"OOSELLMF"` OO OO `xml:"OO"` SellType sellType `xml:"SELLTYPE"` // One of SELL, SELLSHORT UnitType unitType `xml:"UNITTYPE"` // What the units represent: one of SHARES, CURRENCY SellAll Boolean `xml:"SELLALL"` // Sell entire holding } // OrderType returns a string representation of this order's type func (o OOSellMF) OrderType() string { return "OOSELLMF" } // OOSellOpt represents an open order to sell an option type OOSellOpt struct { XMLName xml.Name `xml:"OOSELLOPT"` OO OO `xml:"OO"` OptSellType optSellType `xml:"OPTSELLTYPE"` // One of SELLTOOPEN, SELLTOCLOSE } // OrderType returns a string representation of this order's type func (o OOSellOpt) OrderType() string { return "OOSELLOPT" } // OOSellOther represents an open order to sell a security type not covered by // the other OOSell* elements type OOSellOther struct { XMLName xml.Name `xml:"OOSELLOTHER"` OO OO `xml:"OO"` UnitType unitType `xml:"UNITTYPE"` // What the units represent: one of SHARES, CURRENCY } // OrderType returns a string representation of this order's type func (o OOSellOther) OrderType() string { return "OOSELLOTHER" } // OOSellStock represents an open order to sell stock type OOSellStock struct { XMLName xml.Name `xml:"OOSELLSTOCK"` OO OO `xml:"OO"` SellType sellType `xml:"SELLTYPE"` // One of SELL, SELLSHORT } // OrderType returns a string representation of this order's type func (o OOSellStock) OrderType() string { return "OOSELLSTOCK" } // OOSwitchMF represents an open order to switch to or purchase a different // mutual fund type OOSwitchMF struct { XMLName xml.Name `xml:"SWITCHMF"` OO OO `xml:"OO"` SecID SecurityID `xml:"SECID"` // Security ID of the fund to switch to or purchase UnitType unitType `xml:"UNITTYPE"` // What the units represent: one of SHARES, CURRENCY SwitchAll Boolean `xml:"SWITCHALL"` // Switch entire holding } // OrderType returns a string representation of this order's type func (o OOSwitchMF) OrderType() string { return "SWITCHMF" } // OOList represents a list of open orders (OO* elements) type OOList []OpenOrder // UnmarshalXML handles unmarshalling an OOList element from an XML string func (o *OOList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if startElement, ok := tok.(xml.StartElement); ok { switch startElement.Name.Local { case "OOBUYDEBT": var oo OOBuyDebt if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOBUYMF": var oo OOBuyMF if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOBUYOPT": var oo OOBuyOpt if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOBUYOTHER": var oo OOBuyOther if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOBUYSTOCK": var oo OOBuyStock if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOSELLDEBT": var oo OOSellDebt if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOSELLMF": var oo OOSellMF if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOSELLOPT": var oo OOSellOpt if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOSELLOTHER": var oo OOSellOther if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "OOSELLSTOCK": var oo OOSellStock if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) case "SWITCHMF": var oo OOSwitchMF if err := d.DecodeElement(&oo, &startElement); err != nil { return err } *o = append(*o, OpenOrder(oo)) default: return errors.New("Invalid OOList child tag: " + startElement.Name.Local) } } else { return errors.New("Didn't find an opening element") } } } // 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 // a 401(k) account type ContribSecurity struct { XMLName xml.Name `xml:"CONTRIBSECURITY"` SecID SecurityID `xml:"SECID"` PreTaxContribPct Amount `xml:"PRETAXCONTRIBPCT,omitempty"` // Percentage of each new employee pretax contribution allocated to this security, rate. PreTaxContribAmt Amount `xml:"PRETAXCONTRIBAMT,omitempty"` // Fixed amount of each new employee pretax contribution allocated to this security, amount AfterTaxContribPct Amount `xml:"AFTERTAXCONTRIBPCT,omitempty"` // Percentage of each new employee after tax contribution allocated to this security, rate. AfterTaxContribAmt Amount `xml:"AFTERTAXCONTRIBAMT,omitempty"` // Fixed amount of each new employee pretax contribution allocated to this security, amount. MatchContribPct Amount `xml:"MATCHCONTRIBPCT,omitempty"` // Percentage of each new employer match contribution allocated to this security, rate. MatchContribAmt Amount `xml:"MATCHCONTRIBAMT,omitempty"` // Fixed amount of each new employer match contribution allocated to this security, amount. ProfitSharingContribPct Amount `xml:"PROFITSHARINGCONTRIBPCT,omitempty"` // Percentage of each new employer profit sharing contribution allocated to this security, rate. ProfitSharingContribAmt Amount `xml:"PROFITSHARINGCONTRIBAMT,omitempty"` // Fixed amount of each new employer profit sharing contribution allocated to this security, amount. RolloverContribPct Amount `xml:"ROLLOVERCONTRIBPCT,omitempty"` // Percentage of new rollover contributions allocated to this security, rate. RolloverContribAmt Amount `xml:"ROLLOVERCONTRIBAMT,omitempty"` // Fixed amount of new rollover contributions allocated to this security, amount. OtherVestPct Amount `xml:"OTHERVESTPCT,omitempty"` // Percentage of each new other employer contribution allocated to this security, rate. OtherVestAmt Amount `xml:"OTHERVESTAMT,omitempty"` // Fixed amount of each new other employer contribution allocated to this security, amount. OtherNonVestPct Amount `xml:"OTHERNONVESTPCT,omitempty"` // Percentage of each new other employee contribution allocated to this security, rate. OtherNonVestAmt Amount `xml:"OTHERNONVESTAMT,omitempty"` // Fixed amount of each new other employee contribution allocated to this security, amount } // VestInfo provides the vesting percentage of a 401(k) account as of a // particular date (past, present, or future) type VestInfo struct { XMLName xml.Name `xml:"VESTINFO"` VestDate *Date `xml:"VESTDATE,omitempty"` // Date at which vesting percentage changes. Default (if empty) is that the vesting percentage below applies to the current date VestPct Amount `xml:"VESTPCT"` } // LoanInfo represents a loan outstanding against this 401(k) account type LoanInfo struct { XMLName xml.Name `xml:"VESTINFO"` LoanID String `xml:"LOANID"` // Identifier of this loan LoanDesc String `xml:"LOANDESC,omitempty"` // Loan description InitialLoanBal Amount `xml:"INITIALLOANBAL,omitempty"` // Initial loan balance LoanStartDate *Date `xml:"LOANSTARTDATE,omitempty"` // Start date of loan CurrentLoanBal Amount `xml:"CURRENTLOANBAL"` // Current loan principal balance DtAsOf *Date `xml:"DTASOF"` // Date and time of the current loan balance LoanRate Amount `xml:"LOANRATE,omitempty"` // Loan annual interest rate LoanPmtAmt Amount `xml:"LOANPMTAMT,omitempty"` // Loan payment amount LoanPmtFreq loanPmtFreq `xml:"LOANPMTFREQ,omitempty"` // Frequency of loan repayments: WEEKLY, BIWEEKLY, TWICEMONTHLY, MONTHLY, FOURWEEKS, BIMONTHLY, QUARTERLY, SEMIANNUALLY, ANNUALLY, OTHER. See section 10.2.1 for calculation rules. LoanPmtsInitial Int `xml:"LOANPMTSINITIAL,omitempty"` // Initial number of loan payments. LoanPmtsRemaining Int `xml:"LOANPMTSREMAINING,omitempty"` // Remaining number of loan payments LoanMaturityDate *Date `xml:"LOANMATURITYDATE,omitempty"` // Expected loan end date LoanTotalProjInterest Amount `xml:"LOANTOTALPROJINTEREST,omitempty"` // Total projected interest to be paid on this loan LoanInterestToDate Amount `xml:"LOANINTERESTTODATE,omitempty"` // Total interested paid to date on this loan LoanExtPmtDate *Date `xml:"LOANNEXTPMTDATE,omitempty"` // Next payment due date } // Inv401KSummaryAggregate represents the total of either contributions, // withdrawals, or earnings made in each contribution type in a given period // (dates specified in a containing Inv401KSummaryPeriod) type Inv401KSummaryAggregate struct { XMLName xml.Name // One of CONTRIBUTIONS, WITHDRAWALS, EARNINGS PreTax Amount `xml:"PRETAX,omitempty"` // Pretax contributions, withdrawals, or earlings. AfterTax Amount `xml:"AFTERTAX,omitempty"` // After tax contributions, withdrawals, or earlings. Match Amount `xml:"MATCH,omitempty"` // Employer matching contributions, withdrawals, or earlings. ProfitSharing Amount `xml:"PROFITSHARING,omitempty"` // Profit sharing contributions, withdrawals, or earlings. Rollover Amount `xml:"ROLLOVER,omitempty"` // Rollover contributions, withdrawals, or earlings. OtherVest Amount `xml:"OTHERVEST,omitempty"` // Other vesting contributions, withdrawals, or earlings. OtherNonVest Amount `xml:"OTHERNONVEST,omitempty"` // Other non-vesting contributions, withdrawals, or earlings. Total Amount `xml:"TOTAL"` // Sum of contributions, withdrawals, or earlings from all fund sources. } // Inv401KSummaryPeriod contains the total contributions, withdrawals, and // earnings made in the given date range type Inv401KSummaryPeriod struct { XMLName xml.Name // One of YEARTODATE, INCEPTODATE, or PERIODTODATE DtStart Date `xml:"DTSTART"` DtEnd Date `xml:"DTEND"` Contributions *Inv401KSummaryAggregate `xml:"CONTRIBUTIONS,omitempty"` // 401(k) contribution aggregate. Note: this includes loan payments. Withdrawls *Inv401KSummaryAggregate `xml:"WITHDRAWLS,omitempty"` // 401(k) withdrawals aggregate. Note: this includes loan withdrawals. Earnings *Inv401KSummaryAggregate `xml:"EARNINGS,omitempty"` // 401(k) earnings aggregate. This is the market value change. It includes dividends/interest, and capital gains - realized and unrealized. } // Inv401K is included in InvStatementResponse for 401(k) accounts and provides // a summary of the 401(k) specific information about the user's account. type Inv401K struct { XMLName xml.Name `xml:"INV401K"` EmployerName String `xml:"EMPLOYERNAME"` PlanID String `xml:"PLANID,omitempty"` // Plan number PlanJoinDate *Date `xml:"PLANJOINDATE,omitempty"` // Date the employee joined the plan EmployerContactInfo String `xml:"EMPLOYERCONTACTINFO,omitempty"` // Name of contact person at employer, plus any available contact information, such as phone number BrokerContactInfo String `xml:"BROKERCONTACTINFO,omitempty"` // Name of contact person at broker, plus any available contact information, such as phone number DeferPctPreTax Amount `xml:"DEFERPCTPRETAX,omitempty"` // Percent of employee salary deferred before tax DeferPctAfterTax Amount `xml:"DEFERPCTAFTERTAX,omitempty"` // Percent of employee salary deferred after tax // Aggregate containing employer match information. Absent if employer does not contribute matching funds. MatchPct Amount `xml:"MATCHINFO>MATCHPCT,omitempty"` // Percent of employee contribution matched, e.g., 75% if contribution rate is $0.75/$1.00 MaxMatchAmt Amount `xml:"MATCHINFO>MAXMATCHAMT,omitempty"` // Maximum employer contribution amount in any year MaxMatchPct Amount `xml:"MATCHINFO>MAXMATCHPCT,omitempty"` // Current maximum employer contribution percentage. Maximum match in a year is MAXMATCHPCT up to the MAXMATCHAMT, if provided StartOfYear *Date `xml:"MATCHINFO>STARTOFYEAR,omitempty"` // Specifies when the employer contribution max is reset. Some plans have a maximum based on the company fiscal year rather than calendar year. Assume calendar year if omitted. Only the month and day (MMDD) are used; year (YYYY) and time are ignored BaseMatchAmt Amount `xml:"MATCHINFO>BASEMATCHAMT"` // Specifies a fixed dollar amount contributed by the employer if the employee participates in the plan at all. This may be present in addition to the . $0 if omitted BaseMatchPct Amount `xml:"MATCHINFO>BASEMATCHPCT"` // Specifies a fixed percent of employee salary matched if the employee participates in the plan at all. This may be present in addition to the MATCHPCT>. 0% if omitted. Base match in a year is BASEMATCHPCT up to the BASEMATCHAMT,if provided ContribInfo []ContribSecurity `xml:"CONTRIBINTO>CONTRIBSECURITY"` // Aggregate to describe how new contributions are distributed among the available securities. CurrentVestPct Amount `xml:"CURRENTVESTPCT,omitempty"` // Estimated percentage of employer contributions vested as of the current date. If omitted, assume 100% VestInfo []VestInfo `xml:"VESTINFO,omitempty"` // Vest change dates. Provides the vesting percentage as of any particular past, current, or future date. 0 or more. LoanInfo []LoanInfo `xml:"LOANINFO,omitempty"` // List of any loans outstanding against this account YearToDateSummary Inv401KSummaryPeriod `xml:"INV401KSUMMARY>YEARTODATE"` // Contributions to date for this calendar year. InceptToDateSummary *Inv401KSummaryPeriod `xml:"INV401KSUMMARY>INCEPTODATE,omitempty"` // Total contributions to date (since inception) PeriodToDate *Inv401KSummaryPeriod `xml:"INV401KSUMMARY>PERIODTODATE,omitempty"` // Total contributions this contribution period } // Inv401KBal provides the balances for different 401(k) subaccount types, as // well as the total cash value of the securities held type Inv401KBal struct { XMLName xml.Name `xml:"INV401KBAL"` CashBal Amount `xml:"CASHBAL,omitempty"` // Available cash balance PreTax Amount `xml:"PRETAX,omitempty"` // Current value of all securities purchased with Before Tax Employee contributions AfterTax Amount `xml:"AFTERTAX,omitempty"` // Current value of all securities purchased with After Tax Employee contributions Match Amount `xml:"MATCH,omitempty"` // Current value of all securities purchased with Employer Match contributions ProfitSharing Amount `xml:"PROFITSHARING,omitempty"` // Current value of all securities purchased with Employer Profit Sharing contributions Rollover Amount `xml:"ROLLOVER,omitempty"` // Current value of all securities purchased with Rollover contributions OtherVest Amount `xml:"OTHERVEST,omitempty"` // Current value of all securities purchased with Other (vesting) Employer contributions OtherNonVest Amount `xml:"OTHERNONVEST,omitempty"` // Current value of all securities purchased with Other (non-vesting) Employer contributions Total Amount `xml:"TOTAL"` // Current value of all securities purchased with all contributions BalList []Balance `xml:"BALLIST>BAL,omitempty"` } // InvStatementResponse includes requested transaction, position, open order, // and balance information for an investment account. It is in response to an // InvStatementRequest or sometimes provided as part of an OFX file downloaded // manually from an FI. type InvStatementResponse struct { XMLName xml.Name `xml:"INVSTMTTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` DtAsOf Date `xml:"INVSTMTRS>DTASOF"` CurDef CurrSymbol `xml:"INVSTMTRS>CURDEF"` InvAcctFrom InvAcct `xml:"INVSTMTRS>INVACCTFROM"` InvTranList *InvTranList `xml:"INVSTMTRS>INVTRANLIST,omitempty"` InvPosList PositionList `xml:"INVSTMTRS>INVPOSLIST,omitempty"` InvBal *InvBalance `xml:"INVSTMTRS>INVBAL,omitempty"` InvOOList OOList `xml:"INVSTMTRS>INVOOLIST,omitempty"` MktgInfo String `xml:"INVSTMTRS>MKTGINFO,omitempty"` // Marketing information Inv401K *Inv401K `xml:"INVSTMTRS>INV401K,omitempty"` Inv401KBal *Inv401KBal `xml:"INVSTMTRS>INV401KBAL,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (sr *InvStatementResponse) Name() string { return "INVSTMTTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (sr *InvStatementResponse) Valid(version ofxVersion) (bool, error) { if ok, err := sr.TrnUID.Valid(); !ok { return false, err } //TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (sr *InvStatementResponse) Type() messageType { return InvStmtRs } ================================================ FILE: invstmt_test.go ================================================ package ofxgo import ( "reflect" "strings" "testing" "time" "github.com/aclindsa/xml" ) func TestMarshalInvStatementRequest(t *testing.T) { var expectedString string = ` 20160224131905.000[-5:EST] 1998124 Sup3eSekrit ENG First Bank 01 MYAPP 1234 382827d6-e2d0-4396-bf3b-665979285420 fi.example.com 82736664 20160101000000.000[-5:EST] Y Y Y Y ` var client = BasicClient{ AppID: "MYAPP", AppVer: "1234", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "1998124" request.Signon.UserPass = "Sup3eSekrit" request.Signon.Org = "First Bank" request.Signon.Fid = "01" EST := time.FixedZone("EST", -5*60*60) statementRequest := InvStatementRequest{ TrnUID: "382827d6-e2d0-4396-bf3b-665979285420", InvAcctFrom: InvAcct{ BrokerID: "fi.example.com", AcctID: "82736664", }, DtStart: NewDate(2016, 1, 1, 0, 0, 0, 0, EST), Include: true, IncludeOO: true, IncludePos: true, IncludeBalance: true, } request.InvStmt = append(request.InvStmt, &statementRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() request.Signon.DtClient = *NewDate(2016, 2, 24, 13, 19, 5, 0, EST) marshalCheckRequest(t, &request, expectedString) } func TestUnmarshalInvStatementResponse(t *testing.T) { responseReader := strings.NewReader(` 0 INFO 20170401201244 ENG INVSTRUS 9999 1a0117ad-692b-4c6a-a21b-020d37d34d49 0 INFO 20170331000000 USD invstrus.com 91827364 20170101000000 20170331000000 729483191 20170203 20170207 78462F103 CUSIP 100 229.00 9.00 -22909.00 CASH CASH BUY CREDIT 20170120 20170118 20170123 22000.00 993838 DEPOSIT CHECK 19980 CASH 78462F103 CUSIP CASH LONG 200 235.74 47148.00 20170331160000 Price as of previous close 129887339 CUSIP CASH LONG 1 3 300 20170331160000 16.73 -819.20 0 Sweep Int Rate Current interest rate for sweep account balances PERCENT 0.25 20170401 76464632 922908645 CUSIP 20170310124445 10 CASH GOODTILCANCEL NONE 168.50 BUY SHARES 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 19.75 BUY 78462F103 CUSIP S&P 500 ETF SPY 99184 1.92 OTHER 129887339 CUSIP John's Fertilizer Puts FERTP 882919 PUT 79.00 20170901 100 983322180 CUSIP LARGESTOCK 899422348 CUSIP Whatchamacallit, Inc. WHAT 883897 17 SMALLSTOCK 922908645 CUSIP Mid-Cap Index Fund Admiral Shares VIMAX 99182828 CUSIP Someone's Class B Debt 100.29 COUPON 20170901 QUARTERLY 88181818 CUSIP Foo Bar Don't know what this is `) var expected Response expected.Version = OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *NewDateGMT(2017, 4, 1, 20, 12, 44, 0) expected.Signon.Language = "ENG" expected.Signon.Org = "INVSTRUS" expected.Signon.Fid = "9999" var units1, unitprice1, commission1, total1, amount2 Amount units1.SetFrac64(100, 1) unitprice1.SetFrac64(229, 1) commission1.SetFrac64(9, 1) total1.SetFrac64(-22909, 1) amount2.SetFrac64(22000, 1) invtranlist := InvTranList{ DtStart: *NewDateGMT(2017, 1, 1, 0, 0, 0, 0), DtEnd: *NewDateGMT(2017, 3, 31, 0, 0, 0, 0), InvTransactions: []InvTransaction{ BuyStock{ InvBuy: InvBuy{ InvTran: InvTran{ FiTID: "729483191", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), DtSettle: NewDateGMT(2017, 2, 7, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Commission: commission1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, BuyType: BuyTypeBuy, }, }, BankTransactions: []InvBankTransaction{ { Transactions: []Transaction{ { TrnType: TrnTypeCredit, DtPosted: *NewDateGMT(2017, 1, 20, 0, 0, 0, 0), DtUser: NewDateGMT(2017, 1, 18, 0, 0, 0, 0), DtAvail: NewDateGMT(2017, 1, 23, 0, 0, 0, 0), TrnAmt: amount2, FiTID: "993838", Name: "DEPOSIT", Memo: "CHECK 19980", }, }, SubAcctFund: SubAcctTypeCash, }, }, } var availcash, marginbalance, shortbalance, balvalue Amount availcash.SetFrac64(1673, 100) marginbalance.SetFrac64(-8192, 10) shortbalance.SetFrac64(0, 1) balvalue.SetFrac64(25, 100) invbalance := InvBalance{ AvailCash: availcash, MarginBalance: marginbalance, ShortBalance: shortbalance, BalList: []Balance{ { Name: "Sweep Int Rate", Desc: "Current interest rate for sweep account balances", BalType: BalTypePercent, Value: balvalue, DtAsOf: NewDateGMT(2017, 4, 1, 0, 0, 0, 0), }, }, } var balamt, availbalamt, posunits1, posunitprice1, posmktval1, posunits2, posunitprice2, posmktval2, oounits1, oolimitprice1, oounits2, oolimitprice2 Amount balamt.SetFrac64(20029, 100) availbalamt.SetFrac64(20029, 100) posunits1.SetFrac64(200, 1) posunitprice1.SetFrac64(23574, 100) posmktval1.SetFrac64(47148, 1) posunits2.SetFrac64(1, 1) posunitprice2.SetFrac64(3, 1) posmktval2.SetFrac64(300, 1) oounits1.SetFrac64(10, 1) oolimitprice1.SetFrac64(16850, 100) oounits2.SetFrac64(25, 1) oolimitprice2.SetFrac64(1975, 100) usd, err := NewCurrSymbol("USD") if err != nil { t.Fatalf("Unexpected error creating CurrSymbol for USD\n") } statementResponse := InvStatementResponse{ TrnUID: "1a0117ad-692b-4c6a-a21b-020d37d34d49", Status: Status{ Code: 0, Severity: "INFO", }, DtAsOf: *NewDateGMT(2017, 3, 31, 0, 0, 0, 0), CurDef: *usd, InvAcctFrom: InvAcct{ BrokerID: "invstrus.com", AcctID: "91827364", }, InvTranList: &invtranlist, InvPosList: PositionList{ StockPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits1, UnitPrice: posunitprice1, MktVal: posmktval1, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), Memo: "Price as of previous close", }, }, OptPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "129887339", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits2, UnitPrice: posunitprice2, MktVal: posmktval2, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), }, }, }, InvBal: &invbalance, InvOOList: OOList{ OOBuyMF{ OO: OO{ FiTID: "76464632", SecID: SecurityID{ UniqueID: "922908645", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 10, 12, 44, 45, 0), Units: oounits1, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionNone, LimitPrice: oolimitprice1, }, BuyType: BuyTypeBuy, UnitType: UnitTypeShares, }, OOBuyStock{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, LimitPrice: oolimitprice2, }, BuyType: BuyTypeBuy, }, }, } expected.InvStmt = append(expected.InvStmt, &statementResponse) var yield1, yield2, strikeprice, parvalue Amount yield1.SetFrac64(192, 100) yield2.SetFrac64(17, 1) strikeprice.SetFrac64(79, 1) parvalue.SetFrac64(10029, 100) seclist := SecurityList{ Securities: []Security{ StockInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, SecName: "S&P 500 ETF", Ticker: "SPY", FiID: "99184", }, Yield: yield1, AssetClass: AssetClassOther, }, OptInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "129887339", UniqueIDType: "CUSIP", }, SecName: "John's Fertilizer Puts", Ticker: "FERTP", FiID: "882919", }, OptType: OptTypePut, StrikePrice: strikeprice, DtExpire: *NewDateGMT(2017, 9, 1, 0, 0, 0, 0), ShPerCtrct: 100, SecID: &SecurityID{ UniqueID: "983322180", UniqueIDType: "CUSIP", }, AssetClass: AssetClassLargeStock, }, StockInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, SecName: "Whatchamacallit, Inc.", Ticker: "WHAT", FiID: "883897", }, Yield: yield2, AssetClass: AssetClassSmallStock, }, MFInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "922908645", UniqueIDType: "CUSIP", }, SecName: "Mid-Cap Index Fund Admiral Shares", Ticker: "VIMAX", }, }, DebtInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "99182828", UniqueIDType: "CUSIP", }, SecName: "Someone's Class B Debt", }, ParValue: parvalue, DebtType: DebtTypeCoupon, DtCoupon: NewDateGMT(2017, 9, 1, 0, 0, 0, 0), CouponFreq: CouponFreqQuarterly, }, OtherInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "88181818", UniqueIDType: "CUSIP", }, SecName: "Foo Bar", }, TypeDesc: "Don't know what this is", }, }, } expected.SecList = append(expected.SecList, &seclist) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } checkResponsesEqual(t, &expected, response) checkResponseRoundTrip(t, response) } func TestUnmarshalInvStatementResponse102(t *testing.T) { responseReader := strings.NewReader(`OFXHEADER: 100 DATA: OFXSGML VERSION: 102 SECURITY: NONE ENCODING: USASCII CHARSET: 1252 COMPRESSION: NONE OLDFILEUID: NONE NEWFILEUID: NONE 0 INFO 20170403120000 ENG VV 1000 1000 1283719872 0 INFO 20170403120000 USD www.exampletrader.com 12341234 20161206120000 20170403120000 12341234-20161207-1 20161207120000 20161208120000 SPY161216C00226000 CUSIP -1.0000 0.3500 8.8500 0.2600 200.8900 CASH CASH SELLTOOPEN 100 12341234-20161215-1 20161215120000 20161220120000 78462F10 CUSIP ASSIGN -100.0000 100 CASH 12341234-20161215-2 20161215120000 20161215120000 SPY161216C00226000 CUSIP ASSIGN 1.0000 100 CASH 04956010 CUSIP CASH LONG 100 79.0000 79000 20170403120000 36960410 CUSIP CASH LONG 100.00 29.8700 2987.00 20170403120000 0.0 -0.00 0.00 78462F10 CUSIP SPDR S&P 500 ETF TRUST SPY SPY161216C00226000 CUSIP SPY Dec 16 2016 226.00 Call SPY 161216C00226000 CALL 226.00 20161216120000 100 `) var expected Response expected.Version = OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *NewDateGMT(2017, 4, 3, 12, 0, 0, 0) expected.Signon.Language = "ENG" expected.Signon.Org = "VV" expected.Signon.Fid = "1000" // Ignored 1000 var units1, unitprice1, commission1, fees1, total1, units2, units3 Amount units1.SetFrac64(-1, 1) unitprice1.SetFrac64(35, 100) commission1.SetFrac64(885, 100) fees1.SetFrac64(26, 100) total1.SetFrac64(20089, 100) units2.SetFrac64(-100, 1) units3.SetFrac64(1, 1) invtranlist := InvTranList{ DtStart: *NewDateGMT(2016, 12, 6, 12, 0, 0, 0), DtEnd: *NewDateGMT(2017, 4, 3, 12, 0, 0, 0), InvTransactions: []InvTransaction{ SellOpt{ InvSell: InvSell{ InvTran: InvTran{ FiTID: "12341234-20161207-1", DtTrade: *NewDateGMT(2016, 12, 7, 12, 0, 0, 0), DtSettle: NewDateGMT(2016, 12, 8, 12, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "SPY161216C00226000", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Commission: commission1, Fees: fees1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, OptSellType: OptSellTypeSellToOpen, ShPerCtrct: 100, }, ClosureOpt{ InvTran: InvTran{ FiTID: "12341234-20161215-1", DtTrade: *NewDateGMT(2016, 12, 15, 12, 0, 0, 0), DtSettle: NewDateGMT(2016, 12, 20, 12, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F10", UniqueIDType: "CUSIP", }, OptAction: OptActionAssign, Units: units2, ShPerCtrct: 100, SubAcctSec: SubAcctTypeCash, }, ClosureOpt{ InvTran: InvTran{ FiTID: "12341234-20161215-2", DtTrade: *NewDateGMT(2016, 12, 15, 12, 0, 0, 0), DtSettle: NewDateGMT(2016, 12, 15, 12, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "SPY161216C00226000", UniqueIDType: "CUSIP", }, OptAction: OptActionAssign, Units: units3, ShPerCtrct: 100, SubAcctSec: SubAcctTypeCash, }, }, } var availcash, marginbalance, shortbalance Amount availcash.SetFrac64(0, 1) marginbalance.SetFrac64(-0, 1) shortbalance.SetFrac64(0, 1) invbalance := InvBalance{ AvailCash: availcash, MarginBalance: marginbalance, ShortBalance: shortbalance, } var posunits1, posunitprice1, posmktval1, posunits2, posunitprice2, posmktval2 Amount posunits1.SetFrac64(100, 1) posunitprice1.SetFrac64(79, 1) posmktval1.SetFrac64(79000, 1) posunits2.SetFrac64(100, 1) posunitprice2.SetFrac64(2987, 100) posmktval2.SetFrac64(2987, 1) usd, err := NewCurrSymbol("USD") if err != nil { t.Fatalf("Unexpected error creating CurrSymbol for USD\n") } statementResponse := InvStatementResponse{ TrnUID: "1283719872", Status: Status{ Code: 0, Severity: "INFO", }, DtAsOf: *NewDateGMT(2017, 4, 3, 12, 0, 0, 0), CurDef: *usd, InvAcctFrom: InvAcct{ BrokerID: "www.exampletrader.com", AcctID: "12341234", }, InvTranList: &invtranlist, InvPosList: PositionList{ StockPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "04956010", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits1, UnitPrice: posunitprice1, MktVal: posmktval1, DtPriceAsOf: *NewDateGMT(2017, 4, 3, 12, 0, 0, 0), }, }, StockPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "36960410", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits2, UnitPrice: posunitprice2, MktVal: posmktval2, DtPriceAsOf: *NewDateGMT(2017, 4, 3, 12, 0, 0, 0), }, }, }, InvBal: &invbalance, } expected.InvStmt = append(expected.InvStmt, &statementResponse) var strikeprice Amount strikeprice.SetFrac64(226, 1) seclist := SecurityList{ Securities: []Security{ StockInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "78462F10", UniqueIDType: "CUSIP", }, SecName: "SPDR S&P 500 ETF TRUST", Ticker: "SPY", }, }, OptInfo{ SecInfo: SecInfo{ SecID: SecurityID{ UniqueID: "SPY161216C00226000", UniqueIDType: "CUSIP", }, SecName: "SPY Dec 16 2016 226.00 Call", Ticker: "SPY 161216C00226000", }, OptType: OptTypeCall, StrikePrice: strikeprice, DtExpire: *NewDateGMT(2016, 12, 16, 12, 0, 0, 0), ShPerCtrct: 100, }, }, } expected.SecList = append(expected.SecList, &seclist) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } checkResponsesEqual(t, &expected, response) checkResponseRoundTrip(t, response) } func TestUnmarshalInvTranList(t *testing.T) { input := ` 20170101000000 20170331000000 81818 20170203 20170207 78462F103 CUSIP 100 229.00 9.00 .26 -22090.26 CASH CASH 101.2 81818 20170203 Something to make a memo about 78462F103 CUSIP 100 229.00 -22090.26 CASH CASH BUYTOOPEN 100 129837-1111 20170203 78462F103 CUSIP 0.26 CASH CASH 129837-1112 20170203 78462F103 CUSIP 2300 CASH CASH 129837-1112 20170203 2300 CASH CASH 81818 20170203 78462F103 CUSIP 100 229.00 -22090.26 CASH CASH 129837-1112 20170203 2300 CASH 129837-1111 20170203 78462F103 CUSIP 100 229.00 -22090.26 CASH CASH SELL 129837-1111 20170203 78462F103 CUSIP 2300.00 CASH CASH 129837-1111 20170203 78462F103 CUSIP CASH 100 200 2 1 129837-1111 20170203 78462F103 CUSIP 100 229.00 -22090.26 CASH CASH ` var units1, unitprice1, commission1, fees1, total1, accrdint, total2, oldunits1, newunits1 Amount units1.SetFrac64(100, 1) unitprice1.SetFrac64(229, 1) commission1.SetFrac64(9, 1) fees1.SetFrac64(26, 100) total1.SetFrac64(-2209026, 100) accrdint.SetFrac64(1012, 10) total2.SetFrac64(2300, 1) oldunits1.SetFrac64(100, 1) newunits1.SetFrac64(200, 1) expected := InvTranList{ DtStart: *NewDateGMT(2017, 1, 1, 0, 0, 0, 0), DtEnd: *NewDateGMT(2017, 3, 31, 0, 0, 0, 0), InvTransactions: []InvTransaction{ BuyDebt{ InvBuy: InvBuy{ InvTran: InvTran{ FiTID: "81818", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), DtSettle: NewDateGMT(2017, 2, 7, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Commission: commission1, Fees: fees1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, AccrdInt: accrdint, }, BuyOpt{ InvBuy: InvBuy{ InvTran: InvTran{ FiTID: "81818", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), Memo: "Something to make a memo about", }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, OptBuyType: OptBuyTypeBuyToOpen, ShPerCtrct: 100, }, InvExpense{ InvTran: InvTran{ FiTID: "129837-1111", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Total: fees1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, JrnlSec{ InvTran: InvTran{ FiTID: "129837-1112", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: total2, SubAcctTo: SubAcctTypeCash, SubAcctFrom: SubAcctTypeCash, }, JrnlFund{ InvTran: InvTran{ FiTID: "129837-1112", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, Total: total2, SubAcctTo: SubAcctTypeCash, SubAcctFrom: SubAcctTypeCash, }, BuyOther{ InvBuy: InvBuy{ InvTran: InvTran{ FiTID: "81818", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, }, MarginInterest{ InvTran: InvTran{ FiTID: "129837-1112", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, Total: total2, SubAcctFund: SubAcctTypeCash, }, SellDebt{ InvSell: InvSell{ InvTran: InvTran{ FiTID: "129837-1111", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, SellReason: SellReasonSell, }, RetOfCap{ InvTran: InvTran{ FiTID: "129837-1111", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Total: total2, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, Split{ InvTran: InvTran{ FiTID: "129837-1111", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, SubAcctSec: SubAcctTypeCash, OldUnits: oldunits1, NewUnits: newunits1, Numerator: 2, Denominator: 1, }, SellOther{ InvSell: InvSell{ InvTran: InvTran{ FiTID: "129837-1111", DtTrade: *NewDateGMT(2017, 2, 3, 0, 0, 0, 0), }, SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, Units: units1, UnitPrice: unitprice1, Total: total1, SubAcctSec: SubAcctTypeCash, SubAcctFund: SubAcctTypeCash, }, }, }, } var actual InvTranList err := xml.Unmarshal([]byte(input), &actual) if err != nil { t.Fatalf("Unexpected error unmarshalling InvTranList: %s\n", err) } checkEqual(t, "InvTranList", reflect.ValueOf(&expected), reflect.ValueOf(&actual)) } func TestUnmarshalPositionList(t *testing.T) { input := ` 78462F103 CUSIP CASH LONG 1 3 300 20170331160000 78462F103 CUSIP CASH SHORT 200 235.74 47148.00 20170331160000 Price as of previous close Y 129887339 CUSIP CASH LONG 1 3 300 20170331160000 129887339 CUSIP CASH LONG 1 3 300 20170331160000 78462F103 CUSIP CASH LONG 200 235.74 47148.00 20170331160000 Price as of previous close Y N ` var posunits1, posunitprice1, posmktval1, posunits2, posunitprice2, posmktval2 Amount posunits1.SetFrac64(200, 1) posunitprice1.SetFrac64(23574, 100) posmktval1.SetFrac64(47148, 1) posunits2.SetFrac64(1, 1) posunitprice2.SetFrac64(3, 1) posmktval2.SetFrac64(300, 1) expected := PositionList{ OtherPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits2, UnitPrice: posunitprice2, MktVal: posmktval2, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), }, }, StockPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeShort, Units: posunits1, UnitPrice: posunitprice1, MktVal: posmktval1, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), Memo: "Price as of previous close", }, ReinvDiv: true, }, DebtPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "129887339", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits2, UnitPrice: posunitprice2, MktVal: posmktval2, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), }, }, OptPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "129887339", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits2, UnitPrice: posunitprice2, MktVal: posmktval2, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), }, }, MFPosition{ InvPos: InvPosition{ SecID: SecurityID{ UniqueID: "78462F103", UniqueIDType: "CUSIP", }, HeldInAcct: SubAcctTypeCash, PosType: PosTypeLong, Units: posunits1, UnitPrice: posunitprice1, MktVal: posmktval1, DtPriceAsOf: *NewDateGMT(2017, 3, 31, 16, 0, 0, 0), Memo: "Price as of previous close", }, ReinvDiv: true, ReinvCG: false, }, } var actual PositionList err := xml.Unmarshal([]byte(input), &actual) if err != nil { t.Fatalf("Unexpected error unmarshalling PositionList: %s\n", err) } checkEqual(t, "PositionList", reflect.ValueOf(&expected), reflect.ValueOf(&actual)) } func TestUnmarshalOOList(t *testing.T) { input := ` 76464632 922908645 CUSIP 20170310124445 10 CASH DAY NONE Y 76464632 922908645 CUSIP 20170310124445 10 CASH GOODTILCANCEL NONE 168.50 BUY SHARES 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 19.75 BUYTOCLOSE 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 19.75 BUY 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 19.75 CURRENCY 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE SELLSHORT SHARES Y 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE SELLTOOPEN 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE SHARES 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE SELL 999387423 899422348 CUSIP 20170324031900 25 CASH GOODTILCANCEL ALLORNONE 899422389 CUSIP CURRENCY N ` var oounits1, oolimitprice1, oounits2, oolimitprice2 Amount oounits1.SetFrac64(10, 1) oolimitprice1.SetFrac64(16850, 100) oounits2.SetFrac64(25, 1) oolimitprice2.SetFrac64(1975, 100) expected := OOList{ OOBuyDebt{ OO: OO{ FiTID: "76464632", SecID: SecurityID{ UniqueID: "922908645", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 10, 12, 44, 45, 0), Units: oounits1, SubAcct: SubAcctTypeCash, Duration: DurationDay, Restriction: RestrictionNone, }, Auction: true, }, OOBuyMF{ OO: OO{ FiTID: "76464632", SecID: SecurityID{ UniqueID: "922908645", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 10, 12, 44, 45, 0), Units: oounits1, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionNone, LimitPrice: oolimitprice1, }, BuyType: BuyTypeBuy, UnitType: UnitTypeShares, }, OOBuyOpt{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, LimitPrice: oolimitprice2, }, OptBuyType: OptBuyTypeBuyToClose, }, OOBuyStock{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, LimitPrice: oolimitprice2, }, BuyType: BuyTypeBuy, }, OOBuyOther{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, LimitPrice: oolimitprice2, }, UnitType: UnitTypeCurrency, }, OOSellDebt{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, }, OOSellMF{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, SellType: SellTypeSellShort, UnitType: UnitTypeShares, SellAll: true, }, OOSellOpt{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, OptSellType: OptSellTypeSellToOpen, }, OOSellOther{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, UnitType: UnitTypeShares, }, OOSellStock{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, SellType: SellTypeSell, }, OOSwitchMF{ OO: OO{ FiTID: "999387423", SecID: SecurityID{ UniqueID: "899422348", UniqueIDType: "CUSIP", }, DtPlaced: *NewDateGMT(2017, 3, 24, 3, 19, 0, 0), Units: oounits2, SubAcct: SubAcctTypeCash, Duration: DurationGoodTilCancel, Restriction: RestrictionAllOrNone, }, SecID: SecurityID{ UniqueID: "899422389", UniqueIDType: "CUSIP", }, UnitType: UnitTypeCurrency, SwitchAll: false, }, } var actual OOList err := xml.Unmarshal([]byte(input), &actual) if err != nil { t.Fatalf("Unexpected error unmarshalling OOList: %s\n", err) } checkEqual(t, "OOList", reflect.ValueOf(&expected), reflect.ValueOf(&actual)) } func TestSecurityInfo(t *testing.T) { secInfo := SecInfo{ Ticker: "ABC", } tests := []Security{ DebtInfo{SecInfo: secInfo}, MFInfo{SecInfo: secInfo}, OptInfo{SecInfo: secInfo}, OtherInfo{SecInfo: secInfo}, StockInfo{SecInfo: secInfo}, } for _, tc := range tests { t.Run(tc.SecurityType(), func(t *testing.T) { info := tc.SecurityInfo() if info.Ticker != secInfo.Ticker { t.Errorf("got %v, want %v", info, secInfo) } }) } } func TestInvPosition(t *testing.T) { invPos := InvPosition{ Memo: "stuff", } tests := []Position{ DebtPosition{InvPos: invPos}, MFPosition{InvPos: invPos}, OptPosition{InvPos: invPos}, OtherPosition{InvPos: invPos}, StockPosition{InvPos: invPos}, } for _, tc := range tests { t.Run(tc.PositionType(), func(t *testing.T) { pos := tc.InvPosition() if pos.Memo != invPos.Memo { t.Errorf("got %v, want %v", pos, invPos) } }) } } func TestInvTransaction(t *testing.T) { invTran := InvTran{ Memo: "stuff", } tests := []InvTransaction{ BuyDebt{InvBuy: InvBuy{InvTran: invTran}}, BuyMF{InvBuy: InvBuy{InvTran: invTran}}, BuyOpt{InvBuy: InvBuy{InvTran: invTran}}, BuyOther{InvBuy: InvBuy{InvTran: invTran}}, BuyStock{InvBuy: InvBuy{InvTran: invTran}}, ClosureOpt{InvTran: invTran}, Income{InvTran: invTran}, InvExpense{InvTran: invTran}, JrnlFund{InvTran: invTran}, JrnlSec{InvTran: invTran}, MarginInterest{InvTran: invTran}, Reinvest{InvTran: invTran}, RetOfCap{InvTran: invTran}, SellDebt{InvSell: InvSell{InvTran: invTran}}, SellMF{InvSell: InvSell{InvTran: invTran}}, SellOpt{InvSell: InvSell{InvTran: invTran}}, SellOther{InvSell: InvSell{InvTran: invTran}}, SellStock{InvSell: InvSell{InvTran: invTran}}, Split{InvTran: invTran}, Transfer{InvTran: invTran}, } for _, tc := range tests { t.Run(tc.TransactionType(), func(t *testing.T) { tran := tc.InvTransaction() if tran.Memo != invTran.Memo { t.Errorf("got %v, want %v", tran, invTran) } }) } } ================================================ FILE: leaf_elements.go ================================================ package ofxgo // A list of all the leaf elements in OFX 1.0.3 (the last SGML version of the // spec). These are all the elements that are possibly left unclosed, and which // can have no children of their own. Fortunately these two sets of elements // are the same. We use this list when parsing to remove ambiguities about // element nesting. // // Generated using the following command with the 1.0.3 SPEC .dtd file: // # sed -rn 's/^.*$/\t"\1",/p' *.dtd | sort var ofxLeafElements = []string{ "ACCESSKEY", "ACCRDINT", "ACCTID", "ACCTKEY", "ACCTREQUIRED", "ACCTTYPE", "ADDR1", "ADDR2", "ADDR3", "ADJAMT", "ADJDATE", "ADJDESC", "ADJNO", "APPID", "APPVER", "ASSETCLASS", "AUCTION", "AUTHTOKEN", "AUTHTOKENFIRST", "AUTHTOKENINFOURL", "AUTHTOKENLABEL", "AVAILACCTS", "AVAILCASH", "AVGCOSTBASIS", "BALAMT", "BALCLOSE", "BALDNLD", "BALMIN", "BALOPEN", "BALTYPE", "BANKID", "BILLREFINFO", "BRANCHID", "BROKERID", "BUYPOWER", "BUYTYPE", "CALLPRICE", "CALLTYPE", "CANADDPAYEE", "CANBILLPAY", "CANCELWND", "CANEMAIL", "CANMODMDLS", "CANMODPMTS", "CANMODXFERS", "CANNOTIFY", "CANPENDING", "CANRECUR", "CANSCHED", "CANUSEDESC", "CANUSERANGE", "CASESEN", "CHARTYPE", "CHECKING", "CHECKNUM", "CHGPINFIRST", "CHGUSERINFO", "CHKANDDEB", "CHKERROR", "CHKNUMEND", "CHKNUMSTART", "CHKSTATUS", "CITY", "CLIENTACTREQ", "CLIENTROUTING", "CLIENTUID", "CLIENTUIDREQ", "CLOSINGAVAIL", "CLTCOOKIE", "CODE", "COMMISSION", "CONFMSG", "CORRECTACTION", "CORRECTFITID", "COUNTRY", "COUPONFREQ", "COUPONRT", "CREDITLIMIT", "CSPHONE", "CURDEF", "CURRATE", "CURSYM", "DATEBIRTH", "DAYPHONE", "DAYSTOPAY", "DAYSWITH", "DEBADJ", "DEBTCLASS", "DEBTTYPE", "DENOMINATOR", "DEPANDCREDIT", "DESC", "DFLTDAYSTOPAY", "DIFFFIRSTPMT", "DIFFLASTPMT", "DOMXFERFEE", "DSCAMT", "DSCDATE", "DSCDESC", "DSCRATE", "DTACCTUP", "DTASOF", "DTAUCTION", "DTAVAIL", "DTCALL", "DTCHANGED", "DTCLIENT", "DTCLOSE", "DTCOUPON", "DTCREATED", "DTDUE", "DTEND", "DTEXPIRE", "DTINFOCHG", "DTMAT", "DTNEXT", "DTOPEN", "DTPLACED", "DTPMTDUE", "DTPMTPRC", "DTPOSTED", "DTPOSTEND", "DTPOSTSTART", "DTPRICEASOF", "DTPROFUP", "DTPURCHASE", "DTSERVER", "DTSETTLE", "DTSTART", "DTTRADE", "DTUSER", "DTXFERPRC", "DTXFERPRJ", "DTYIELDASOF", "DURATION", "EMAIL", "EVEPHONE", "EXTDPMTCHK", "EXTDPMTFOR", "FAXPHONE", "FEE", "FEEMSG", "FEES", "FIASSETCLASS", "FICERTID", "FID", "FIID", "FINALAMT", "FINAME", "FINCHG", "FIRSTNAME", "FITID", "FRACCASH", "FREQ", "FROM", "GAIN", "GENUSERKEY", "GETMIMESUP", "HASEXTDPMT", "HELDINACCT", "IDSCOPE", "INCBAL", "INCIMAGES", "INCLUDE", "INCOMETYPE", "INCOO", "INITIALAMT", "INTLXFERFEE", "INVACCTTYPE", "INVALIDACCTTYPE", "INVDATE", "INVDESC", "INVNO", "INVPAIDAMT", "INVTOTALAMT", "LANGUAGE", "LASTNAME", "LIMITPRICE", "LITMAMT", "LITMDESC", "LOAD", "LOSTSYNC", "MAILSUP", "MARGINBALANCE", "MARKDOWN", "MARKUP", "MAX", "MEMO", "MESSAGE", "MFACHALLENGEFIRST", "MFACHALLENGESUPT", "MFAPHRASEA", "MFAPHRASEID", "MFAPHRASELABEL", "MFTYPE", "MIDDLENAME", "MIN", "MINPMTDUE", "MINUNITS", "MKTGINFO", "MKTVAL", "MODELWND", "MODPENDING", "NAME", "NEWUNITS", "NEWUSERPASS", "NINSTS", "NONCE", "NUMERATOR", "OFXSEC", "OLDUNITS", "OODNLD", "OPTACTION", "OPTBUYTYPE", "OPTIONLEVEL", "OPTSELLTYPE", "OPTTYPE", "ORG", "PARVALUE", "PAYACCT", "PAYANDCREDIT", "PAYEEID", "PAYEELSTID", "PAYINSTRUCT", "PERCENT", "PHONE", "PINCH", "PMTBYADDR", "PMTBYPAYEEID", "PMTBYXFER", "PMTPRCCODE", "POSDNLD", "POSTALCODE", "POSTPROCWND", "POSTYPE", "PROCDAYSOFF", "PROCENDTM", "PURANDADV", "RATING", "RECSRVRTID", "REFNUM", "REFRESH", "REFRESHSUPT", "REINVCG", "REINVDIV", "REJECTIFMISSING", "RELFITID", "RELTYPE", "RESPFILEER", "RESTRICTION", "SECLISTRQDNLD", "SECNAME", "SECURED", "SECURITYNAME", "SELLALL", "SELLREASON", "SELLTYPE", "SESSCOOKIE", "SEVERITY", "SHORTBALANCE", "SHPERCTRCT", "SIC", "SIGNONREALM", "SPACES", "SPECIAL", "SPNAME", "SRVRTID", "STATE", "STOCKTYPE", "STOPPRICE", "STPCHKFEE", "STRIKEPRICE", "STSVIAMODS", "SUBACCT", "SUBACCTFROM", "SUBACCTSEC", "SUBACCTTO", "SUBJECT", "SUPTXDL", "SVC", "SVCSTATUS", "SWITCHALL", "SYNCMODE", "TAN", "TAXES", "TAXEXEMPT", "TAXID", "TEMPPASS", "TFERACTION", "TICKER", "TO", "TOKEN", "TOKENONLY", "TOTAL", "TOTALFEES", "TOTALINT", "TRANDNLD", "TRANSPSEC", "TRNAMT", "TRNTYPE", "TRNUID", "TSKEYEXPIRE", "TSPHONE", "TYPEDESC", "UNIQUEID", "UNIQUEIDTYPE", "UNITPRICE", "UNITS", "UNITSSTREET", "UNITSUSER", "UNITTYPE", "URL", "USEHTML", "USERCRED1", "USERCRED1LABEL", "USERCRED2", "USERCRED2LABEL", "USERID", "USERKEY", "USERPASS", "USPRODUCTTYPE", "VALUE", "VER", "WITHHOLDING", "XFERDAYSWITH", "XFERDEST", "XFERDFLTDAYSTOPAY", "XFERPRCCODE", "XFERSRC", "YIELD", "YIELDTOCALL", "YIELDTOMAT", } ================================================ FILE: profile.go ================================================ package ofxgo import ( "errors" "github.com/aclindsa/xml" "strings" ) // ProfileRequest represents a request for a server to provide a profile of its // capabilities (which message sets and versions it supports, how to access // them, which languages and which types of synchronization they support, etc.) type ProfileRequest struct { XMLName xml.Name `xml:"PROFTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` ClientRouting String `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE DtProfUp Date `xml:"PROFRQ>DTPROFUP"` // Date and time client last received a profile update } // Name returns the name of the top-level transaction XML/SGML element func (r *ProfileRequest) Name() string { return "PROFTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *ProfileRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement r.ClientRouting = "NONE" return true, nil } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *ProfileRequest) Type() messageType { return ProfRq } // SignonInfo provides the requirements to login to a single signon realm. A // signon realm consists of all MessageSets which can be accessed using one set // of login credentials. Most FI's only use one signon realm to make it easier // and less confusing for the user. type SignonInfo struct { XMLName xml.Name `xml:"SIGNONINFO"` SignonRealm String `xml:"SIGNONREALM"` // The SignonRealm for which this SignonInfo provides information. This SignonInfo is valid for all MessageSets with SignonRealm fields matching this one Min Int `xml:"MIN"` // Minimum number of password characters Max Int `xml:"MAX"` // Maximum number of password characters CharType charType `xml:"CHARTYPE"` // One of ALPHAONLY, NUMERICONLY, ALPHAORNUMERIC, ALPHAANDNUMERIC CaseSen Boolean `xml:"CASESEN"` // Password is case-sensitive? Special Boolean `xml:"SPECIAL"` // Special characters allowed? Spaces Boolean `xml:"SPACES"` // Spaces allowed? PinCh Boolean `xml:"PINCH"` // Pin change requests allowed ChgPinFirst Boolean `xml:"CHGPINFIRST"` // Server requires user to change password at first signon UserCred1Label String `xml:"USERCRED1LABEL,omitempty"` // Prompt for USERCRED1 (if this field is present, USERCRED1 is required) UserCred2Label String `xml:"USERCRED2LABEL,omitempty"` // Prompt for USERCRED2 (if this field is present, USERCRED2 is required) ClientUIDReq Boolean `xml:"CLIENTUIDREQ,omitempty"` // CLIENTUID required? AuthTokenFirst Boolean `xml:"AUTHTOKENFIRST,omitempty"` // Server requires AUTHTOKEN as part of first signon AuthTokenLabel String `xml:"AUTHTOKENLABEL,omitempty"` AuthTokenInfoURL String `xml:"AUTHTOKENINFOURL,omitempty"` MFAChallengeSupt Boolean `xml:"MFACHALLENGESUPT,omitempty"` // Server supports MFACHALLENGE MFAChallengeFIRST Boolean `xml:"MFACHALLENGEFIRST,omitempty"` // Server requires MFACHALLENGE to be sent with first signon AccessTokenReq Boolean `xml:"ACCESSTOKENREQ,omitempty"` // Server requires ACCESSTOKEN to be sent with all requests except profile } // MessageSet represents one message set supported by an FI and its // capabilities type MessageSet struct { XMLName xml.Name // Name string // (copy of XMLName.Local) Ver Int `xml:"MSGSETCORE>VER"` // Message set version - should always match 'n' in of Name URL String `xml:"MSGSETCORE>URL"` // URL where messages in this set are to be set OfxSec ofxSec `xml:"MSGSETCORE>OFXSEC"` // NONE or 'TYPE 1' TranspSec Boolean `xml:"MSGSETCORE>TRANSPSEC"` // Transport-level security must be used SignonRealm String `xml:"MSGSETCORE>SIGNONREALM"` // Used to identify which SignonInfo to use for to this MessageSet Language []String `xml:"MSGSETCORE>LANGUAGE"` // List of supported languages SyncMode syncMode `xml:"MSGSETCORE>SYNCMODE"` // One of FULL, LITE RefreshSupt Boolean `xml:"MSGSETCORE>REFRESHSUPT,omitempty"` // Y if server supports Y within synchronizations. This option is irrelevant for full synchronization servers. Clients must ignore (or its absence) if the profile also specifies FULL. For lite synchronization, the default is N. Without Y, lite synchronization servers are not required to support Y requests RespFileER Boolean `xml:"MSGSETCORE>RESPFILEER"` // server supports file-based error recovery SpName String `xml:"MSGSETCORE>SPNAME"` // Name of service provider // TODO MessageSet-specific stuff? } // MessageSetList is a list of MessageSets (necessary because they must be // manually parsed) type MessageSetList []MessageSet // UnmarshalXML handles unmarshalling a MessageSetList element from an XML string func (msl *MessageSetList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { var msgset MessageSet tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if _, ok := tok.(xml.StartElement); ok { // Found starting tag for . Get the next one (xxxMSGSETVn) and decode that struct tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if versionStart, ok := tok.(xml.StartElement); ok { if err := d.DecodeElement(&msgset, &versionStart); err != nil { return err } } else { return errors.New("Invalid MSGSETLIST formatting") } msgset.Name = msgset.XMLName.Local // Eat ending tags for tok, err = nextNonWhitespaceToken(d) if err != nil { return err } else if _, ok := tok.(xml.EndElement); !ok { return errors.New("Invalid MSGSETLIST formatting") } } else { return errors.New("MSGSETLIST didn't find an opening xxxMSGSETVn element") } *msl = MessageSetList(append(*(*[]MessageSet)(msl), msgset)) } } // 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 // (which message sets and versions it supports, how to access them, which // languages and which types of synchronization they support, etc.). Note that // if the server does not support ClientRouting=NONE (as we always send with // ProfileRequest), this may be an error) type ProfileResponse struct { XMLName xml.Name `xml:"PROFTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` MessageSetList MessageSetList `xml:"PROFRS>MSGSETLIST"` SignonInfoList []SignonInfo `xml:"PROFRS>SIGNONINFOLIST>SIGNONINFO"` DtProfUp Date `xml:"PROFRS>DTPROFUP"` FiName String `xml:"PROFRS>FINAME"` Addr1 String `xml:"PROFRS>ADDR1"` Addr2 String `xml:"PROFRS>ADDR2,omitempty"` Addr3 String `xml:"PROFRS>ADDR3,omitempty"` City String `xml:"PROFRS>CITY"` State String `xml:"PROFRS>STATE"` PostalCode String `xml:"PROFRS>POSTALCODE"` Country String `xml:"PROFRS>COUNTRY"` CsPhone String `xml:"PROFRS>CSPHONE,omitempty"` TsPhone String `xml:"PROFRS>TSPHONE,omitempty"` FaxPhone String `xml:"PROFRS>FAXPHONE,omitempty"` URL String `xml:"PROFRS>URL,omitempty"` Email String `xml:"PROFRS>EMAIL,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (pr *ProfileResponse) Name() string { return "PROFTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (pr *ProfileResponse) Valid(version ofxVersion) (bool, error) { if ok, err := pr.TrnUID.Valid(); !ok { return false, err } //TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (pr *ProfileResponse) Type() messageType { return ProfRs } ================================================ FILE: profile_test.go ================================================ package ofxgo import ( "strings" "testing" "time" ) func TestMarshalProfileRequest(t *testing.T) { var expectedString string = ` 20160614073400.000[-5:EST] anonymous00000000000000000000000 anonymous00000000000000000000000 ENG BNK 1987 OFXGO 0001 983373 NONE 20160101000000.000[-5:EST] ` var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "anonymous00000000000000000000000" request.Signon.UserPass = "anonymous00000000000000000000000" request.Signon.Org = "BNK" request.Signon.Fid = "1987" EST := time.FixedZone("EST", -5*60*60) profileRequest := ProfileRequest{ TrnUID: "983373", DtProfUp: *NewDate(2016, 1, 1, 0, 0, 0, 0, EST), } request.Prof = append(request.Prof, &profileRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() request.Signon.DtClient = *NewDate(2016, 6, 14, 7, 34, 0, 0, EST) marshalCheckRequest(t, &request, expectedString) } func TestUnmarshalProfileResponse102(t *testing.T) { responseReader := strings.NewReader(`OFXHEADER:100 DATA:OFXSGML VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 0 INFO 20170403093458.000 ENG 20021119140000 0f94ce83-13b7-7568-e4fc-c02c7b47e7ab 0 INFO 1 https://ofx.example.com/cgi-ofx/exampleofx NONE Y Example Trade ENG LITE N 300 1 https://ofx.example.com/cgi-ofx/exampleofx NONE Y Example Trade ENG LITE N 300 Y N Y Y 1 https://ofx.example.com/cgi-ofx/exampleofx NONE Y Example Trade ENG LITE N 300 Y N Y Y N 1 https://ofx.example.com/cgi-ofx/exampleofx NONE Y Example Trade ENG LITE N 300 Y 1 https://ofx.example.com/cgi-ofx/exampleofx NONE Y Example Trade ENG LITE N 300 Example Trade 1 32 ALPHAORNUMERIC N Y N N N 20021119140000 Example Trade Financial 5555 Buhunkus Drive Someville NC 28801 USA 1-800-234-5678 1-800-234-5678 1-888-234-5678 http://www.example.com service@example.com example.com `) var expected Response expected.Version = OfxVersion102 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *NewDateGMT(2017, 4, 3, 9, 34, 58, 0) expected.Signon.Language = "ENG" expected.Signon.DtProfUp = NewDateGMT(2002, 11, 19, 14, 0, 0, 0) profileResponse := ProfileResponse{ TrnUID: "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab", Status: Status{ Code: 0, Severity: "INFO", }, MessageSetList: MessageSetList{ MessageSet{ Name: "SIGNONMSGSETV1", Ver: 1, URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", Language: []String{"ENG"}, SyncMode: SyncModeLite, RespFileER: false, // Ignored: 300 }, MessageSet{ Name: "SIGNUPMSGSETV1", Ver: 1, URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", Language: []String{"ENG"}, SyncMode: SyncModeLite, RespFileER: false, // Ignored: 300 }, MessageSet{ Name: "INVSTMTMSGSETV1", Ver: 1, URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", Language: []String{"ENG"}, SyncMode: SyncModeLite, RespFileER: false, // Ignored: 300 }, MessageSet{ Name: "SECLISTMSGSETV1", Ver: 1, URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", Language: []String{"ENG"}, SyncMode: SyncModeLite, RespFileER: false, // Ignored: 300 }, MessageSet{ Name: "PROFMSGSETV1", Ver: 1, URL: "https://ofx.example.com/cgi-ofx/exampleofx", OfxSec: OfxSecNone, TranspSec: true, SignonRealm: "Example Trade", Language: []String{"ENG"}, SyncMode: SyncModeLite, RespFileER: false, // Ignored: 300 }, }, SignonInfoList: []SignonInfo{ { SignonRealm: "Example Trade", Min: 1, Max: 32, CharType: CharTypeAlphaOrNumeric, CaseSen: false, Special: true, Spaces: false, PinCh: false, ChgPinFirst: false, }, }, DtProfUp: *NewDateGMT(2002, 11, 19, 14, 0, 0, 0), FiName: "Example Trade Financial", Addr1: "5555 Buhunkus Drive", City: "Someville", State: "NC", PostalCode: "28801", Country: "USA", CsPhone: "1-800-234-5678", TsPhone: "1-800-234-5678", FaxPhone: "1-888-234-5678", URL: "http://www.example.com", Email: "service@example.com", // Ignored: example.com } expected.Prof = append(expected.Prof, &profileResponse) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } checkResponsesEqual(t, &expected, response) checkResponseRoundTrip(t, response) } ================================================ FILE: request.go ================================================ package ofxgo import ( "bytes" "errors" "github.com/aclindsa/xml" "time" ) // Request is the top-level object marshalled and sent to OFX servers. It is // constructed by appending one or more request objects to the message set they // correspond to (i.e. appending StatementRequest to Request.Bank to get a bank // statemement). If a *Request object is appended to the wrong message set, an // error will be returned when Marshal() is called on this Request. type Request struct { URL string Version ofxVersion // OFX version, overwritten in Client.Request() Signon SignonRequest // Signup []Message // Bank []Message // CreditCard []Message // Loan []Message // InvStmt []Message // InterXfer []Message // WireXfer []Message // Billpay []Message // Email []Message // SecList []Message // PresDir []Message // PresDlv []Message // Prof []Message // Image []Message // indent bool // Whether to indent the marshaled XML carriageReturn bool // Whether to user carriage returns in new lines for marshaled XML } func encodeMessageSet(e *xml.Encoder, requests []Message, set messageType, version ofxVersion) error { if len(requests) > 0 { messageSetElement := xml.StartElement{Name: xml.Name{Local: set.String()}} if err := e.EncodeToken(messageSetElement); err != nil { return err } for _, request := range requests { if request.Type() != set { return errors.New("Expected " + set.String() + " message , found " + request.Type().String()) } if ok, err := request.Valid(version); !ok { return err } if err := e.Encode(request); err != nil { return err } } if err := e.EncodeToken(messageSetElement.End()); err != nil { return err } } return nil } // SetClientFields overwrites the fields in this Request object controlled by // the Client func (oq *Request) SetClientFields(c Client) { oq.Signon.DtClient.Time = time.Now() // Overwrite fields that the client controls oq.Version = c.OfxVersion() oq.Signon.AppID = c.ID() oq.Signon.AppVer = c.Version() oq.indent = c.IndentRequests() oq.carriageReturn = c.CarriageReturnNewLines() } // Marshal this Request 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 server func (oq *Request) Marshal() (*bytes.Buffer, error) { var b bytes.Buffer // Write the header appropriate to our version writeHeader(&b, oq.Version, oq.carriageReturn) encoder := xml.NewEncoder(&b) if oq.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"}} if err := encoder.EncodeToken(ofxElement); err != nil { return nil, err } if ok, err := oq.Signon.Valid(oq.Version); !ok { return nil, err } signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRq.String()}} if err := encoder.EncodeToken(signonMsgSet); err != nil { return nil, err } if err := encoder.Encode(&oq.Signon); err != nil { return nil, err } if err := encoder.EncodeToken(signonMsgSet.End()); err != nil { return nil, err } messageSets := []struct { Messages []Message Type messageType }{ {oq.Signup, SignupRq}, {oq.Bank, BankRq}, {oq.CreditCard, CreditCardRq}, {oq.Loan, LoanRq}, {oq.InvStmt, InvStmtRq}, {oq.InterXfer, InterXferRq}, {oq.WireXfer, WireXferRq}, {oq.Billpay, BillpayRq}, {oq.Email, EmailRq}, {oq.SecList, SecListRq}, {oq.PresDir, PresDirRq}, {oq.PresDlv, PresDlvRq}, {oq.Prof, ProfRq}, {oq.Image, ImageRq}, } for _, set := range messageSets { if err := encodeMessageSet(encoder, set.Messages, set.Type, oq.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 } ================================================ FILE: request_test.go ================================================ package ofxgo import ( "regexp" "strings" "testing" ) // match leading and trailing whitespace on each line var ignoreSpacesRe = regexp.MustCompile("(?m)^[ \t]+|[ \t]*$[\r\n]+") func marshalCheckRequest(t *testing.T, request *Request, expected string) { t.Helper() buf, err := request.Marshal() if err != nil { t.Fatalf("%s: Unexpected error marshalling request: %s\n", t.Name(), err) } actualString := buf.String() // Ignore spaces between XML elements expectedString := ignoreSpacesRe.ReplaceAllString(expected, "") actualString = ignoreSpacesRe.ReplaceAllString(actualString, "") if expectedString != actualString { compareLength := len(expectedString) if len(actualString) < compareLength { compareLength = len(actualString) } for i := 0; i < compareLength; i++ { if expectedString[i] != actualString[i] { firstDifferencePosition := 13 displayStart := i - 10 prefix := "..." suffix := "..." if displayStart < 0 { prefix = "" firstDifferencePosition = i displayStart = 0 } displayEnd := displayStart + 40 if displayEnd > compareLength { suffix = "" displayEnd = compareLength } t.Fatalf("%s expected '%s%s%s',\ngot '%s%s%s'\n %s^ first difference\n", t.Name(), prefix, expectedString[displayStart:displayEnd], suffix, prefix, actualString[displayStart:displayEnd], suffix, strings.Repeat(" ", firstDifferencePosition)) } } if len(actualString) > compareLength { t.Fatalf("%s: Actual string longer than expected string\n", t.Name()) } else { t.Fatalf("%s: Actual string shorter than expected string\n", t.Name()) } } } ================================================ FILE: response.go ================================================ package ofxgo import ( "bufio" "bytes" "errors" "fmt" "io" "reflect" "regexp" "strings" "github.com/aclindsa/xml" ) // Response is the top-level object returned from a parsed OFX response file. // It can be inspected by using type assertions or switches on the message set // you're interested in. type Response struct { Version ofxVersion // OFX header version Signon SignonResponse // Signup []Message // Bank []Message // CreditCard []Message // Loan []Message // InvStmt []Message // InterXfer []Message // WireXfer []Message // Billpay []Message // Email []Message // SecList []Message // PresDir []Message // PresDlv []Message // Prof []Message // Image []Message // } func (or *Response) readSGMLHeaders(r *bufio.Reader) error { b, err := r.ReadSlice('<') if err != nil { return err } s := string(b) err = r.UnreadByte() if err != nil { return err } // According to the latest OFX SGML spec (1.6), headers should be CRLF-separated // and written as KEY:VALUE. However, some banks include a whitespace after the // colon (KEY: VALUE), while others include no line breaks at all. The spec doesn't // require a line break after the OFX headers, but it is allowed, and will be // optionally captured & discarded by the trailing `\s*`. Valid SGML headers must // always be present in exactly this order, so a regular expression is acceptable. headerExp := regexp.MustCompile( `^OFXHEADER:\s*(?P\d+)\s*` + `DATA:\s*(?P[A-Z]+)\s*` + `VERSION:\s*(?P\d+)\s*` + `SECURITY:\s*(?P[\w]+)\s*` + `ENCODING:\s*(?P[A-Z0-9-]+)\s*` + `CHARSET:\s*(?P[\w-]+)\s*` + `COMPRESSION:\s*(?P[A-Z]+)\s*` + `OLDFILEUID:\s*(?P[\w-]+)\s*` + `NEWFILEUID:\s*(?P[\w-]+)\s*<$`) matches := headerExp.FindStringSubmatch(s) if len(matches) == 0 { return errors.New("OFX headers malformed") } for i, name := range headerExp.SubexpNames() { if i == 0 { continue } headerValue := matches[i] switch name { case "OFXHEADER": if headerValue != "100" { return errors.New("OFXHEADER is not 100") } case "DATA": if headerValue != "OFXSGML" { return errors.New("OFX DATA header does not contain OFXSGML") } case "VERSION": err := or.Version.FromString(headerValue) if err != nil { return err } if or.Version > OfxVersion160 { return errors.New("OFX VERSION > 160 in SGML header") } case "SECURITY": if !(headerValue == "NONE" || headerValue == "TYPE1") { return errors.New("OFX SECURITY header must be NONE or TYPE1") } case "COMPRESSION": if headerValue != "NONE" { return errors.New("OFX COMPRESSION header not NONE") } case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID": // TODO: check/handle these headers? } } return nil } func (or *Response) readXMLHeaders(decoder *xml.Decoder) error { var tok xml.Token tok, err := nextNonWhitespaceToken(decoder) if err != nil { return err } else if xmlElem, ok := tok.(xml.ProcInst); !ok || xmlElem.Target != "xml" { return errors.New("Missing xml processing instruction") } // parse the OFX header tok, err = nextNonWhitespaceToken(decoder) if err != nil { return err } else if ofxElem, ok := tok.(xml.ProcInst); ok && ofxElem.Target == "OFX" { var seenHeader, seenVersion bool = false, false headers := bytes.TrimSpace(ofxElem.Inst) for len(headers) > 0 { tmp := bytes.SplitN(headers, []byte("=\""), 2) if len(tmp) != 2 { return errors.New("Malformed OFX header") } header := string(tmp[0]) headers = tmp[1] tmp = bytes.SplitN(headers, []byte("\""), 2) if len(tmp) != 2 { return errors.New("Malformed OFX header") } value := string(tmp[0]) headers = bytes.TrimSpace(tmp[1]) switch header { case "OFXHEADER": if value != "200" { return errors.New("OFXHEADER is not 200") } seenHeader = true case "VERSION": err := or.Version.FromString(value) if err != nil { return err } seenVersion = true if or.Version < OfxVersion200 { return errors.New("OFX VERSION < 200 in XML header") } case "SECURITY": if value != "NONE" { return errors.New("OFX SECURITY header not NONE") } case "OLDFILEUID", "NEWFILEUID": // TODO check/handle these headers? default: return errors.New("Invalid OFX header: " + header) } } if !seenHeader { return errors.New("OFXHEADER version missing") } if !seenVersion { return errors.New("OFX VERSION header missing") } } else { return errors.New("Missing xml 'OFX' processing instruction") } return nil } // Number of bytes of response to read when attempting to figure out whether // we're using OFX or SGML const guessVersionCheckBytes = 1024 // Defaults to XML if it can't determine the version or if there is any // ambiguity // Returns false for SGML, true (for XML) otherwise. func guessVersion(r *bufio.Reader) (bool, error) { b, _ := r.Peek(guessVersionCheckBytes) if b == nil { return false, errors.New("Failed to read OFX header") } sgmlIndex := bytes.Index(b, []byte("OFXHEADER:")) xmlIndex := bytes.Index(b, []byte("OFXHEADER=")) if sgmlIndex < 0 { return true, nil } else if xmlIndex < 0 { return false, nil } else { return xmlIndex <= sgmlIndex, nil } } // A map of message set tags to a map of transaction wrapper tags to the // reflect.Type of the struct for that transaction type. Used when decoding // Responses. Newly-implemented response transaction types *must* be added to // this map in order to be unmarshalled. var responseTypes = map[string]map[string]reflect.Type{ SignupRs.String(): { (&AcctInfoResponse{}).Name(): reflect.TypeOf(AcctInfoResponse{})}, BankRs.String(): { (&StatementResponse{}).Name(): reflect.TypeOf(StatementResponse{})}, CreditCardRs.String(): { (&CCStatementResponse{}).Name(): reflect.TypeOf(CCStatementResponse{})}, LoanRs.String(): {}, InvStmtRs.String(): { (&InvStatementResponse{}).Name(): reflect.TypeOf(InvStatementResponse{})}, InterXferRs.String(): {}, WireXferRs.String(): {}, BillpayRs.String(): {}, EmailRs.String(): {}, SecListRs.String(): { (&SecListResponse{}).Name(): reflect.TypeOf(SecListResponse{}), (&SecurityList{}).Name(): reflect.TypeOf(SecurityList{})}, PresDirRs.String(): {}, PresDlvRs.String(): {}, ProfRs.String(): { (&ProfileResponse{}).Name(): reflect.TypeOf(ProfileResponse{})}, ImageRs.String(): {}, } func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, version ofxVersion) error { setTypes, ok := responseTypes[start.Name.Local] if !ok { return errors.New("Invalid message set: " + start.Name.Local) } for { tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if startElement, ok := tok.(xml.StartElement); ok { responseType, ok := setTypes[startElement.Name.Local] if !ok { // If you are a developer and received this message after you // thought you added a new transaction type, make sure you // added it to the responseTypes map above return errors.New("Unsupported response transaction for " + start.Name.Local + ": " + startElement.Name.Local) } response := reflect.New(responseType).Interface() responseMessage := response.(Message) if err := d.DecodeElement(responseMessage, &startElement); err != nil { return err } *msgs = append(*msgs, responseMessage) } else { return errors.New("Didn't find an opening element") } } } // ParseResponse parses and validates an OFX response in SGML or XML into a // Response object from the given io.Reader // // 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 // performs version autodetection if it can and attempts to be as forgiving as // possible about the input format. 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 r := bufio.NewReaderSize(reader, guessVersionCheckBytes) xmlVersion, err := guessVersion(r) if err != nil { return nil, err } // parse SGML headers before creating XML decoder if !xmlVersion { if err := or.readSGMLHeaders(r); err != nil { return nil, err } } decoder := xml.NewDecoder(r) if !xmlVersion { decoder.Strict = false decoder.AutoCloseAfterCharData = ofxLeafElements } decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } if xmlVersion { // parse the xml header if err := or.readXMLHeaders(decoder); err != nil { return nil, err } } tok, err := nextNonWhitespaceToken(decoder) if err != nil { return nil, err } else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" { return nil, errors.New("Missing opening OFX xml element") } // Unmarshal the signon message tok, err = nextNonWhitespaceToken(decoder) if err != nil { return nil, err } else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == SignonRs.String() { if err := decoder.Decode(&or.Signon); err != nil { return nil, err } } else { return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element") } tok, err = nextNonWhitespaceToken(decoder) if err != nil { return nil, err } else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() { return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element") } var messageSlices = map[string]*[]Message{ SignupRs.String(): &or.Signup, BankRs.String(): &or.Bank, CreditCardRs.String(): &or.CreditCard, LoanRs.String(): &or.Loan, InvStmtRs.String(): &or.InvStmt, InterXferRs.String(): &or.InterXfer, WireXferRs.String(): &or.WireXfer, BillpayRs.String(): &or.Billpay, EmailRs.String(): &or.Email, SecListRs.String(): &or.SecList, PresDirRs.String(): &or.PresDir, PresDlvRs.String(): &or.PresDlv, ProfRs.String(): &or.Prof, ImageRs.String(): &or.Image, } for { tok, err = nextNonWhitespaceToken(decoder) if err != nil { return nil, err } else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" { return &or, nil // found closing XML element, so we're done } else if start, ok := tok.(xml.StartElement); ok { slice, ok := messageSlices[start.Name.Local] if !ok { return nil, errors.New("Invalid message set: " + start.Name.Local) } if err := decodeMessageSet(decoder, start, slice, or.Version); err != nil { return nil, err } } else { return nil, errors.New("Found unexpected token") } } } // 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 } ================================================ FILE: response_test.go ================================================ package ofxgo import ( "bytes" "errors" "fmt" "os" "path/filepath" "reflect" "testing" "github.com/aclindsa/xml" ) // Attempt to find a method on the provided Value called 'Equal' which is a // receiver for the Value, takes one argument of the same type, and returns // one bool. equalMethodOf() returns the nil value if the method couldn't be // found. func equalMethodOf(v reflect.Value) reflect.Value { if equalMethod, ok := v.Type().MethodByName("Equal"); ok { if !equalMethod.Func.IsNil() && equalMethod.Type.NumIn() == 2 && equalMethod.Type.In(0) == v.Type() && equalMethod.Type.In(1) == v.Type() && equalMethod.Type.NumOut() == 1 && equalMethod.Type.Out(0).Kind() == reflect.Bool { return v.MethodByName("Equal") } } return reflect.ValueOf(nil) } // Attempt to return a string representation of the value appropriate for its // type by finding a method on the provided Value called 'String' which is a // receiver for the Value, and returns one string. stringMethodOf() returns // fmt.Sprintf("%s", v) if it can't find a String method. func valueToString(v reflect.Value) string { if equalMethod, ok := v.Type().MethodByName("String"); ok { if !equalMethod.Func.IsNil() && equalMethod.Type.NumIn() == 1 && equalMethod.Type.In(0) == v.Type() && equalMethod.Type.NumOut() == 1 && equalMethod.Type.Out(0).Kind() == reflect.String { out := v.MethodByName("String").Call([]reflect.Value{}) return out[0].String() } } return fmt.Sprintf("%s", v) } // Recursively check that the expected and actual Values are equal in value. // If the two Values are equal in type and contain an appropriate Equal() // method (see equalMethodOf()), that method is used for comparison. The // provided testing.T is failed with a message if any inequality is found. func checkEqual(t *testing.T, fieldName string, expected, actual reflect.Value) { if expected.IsValid() && !actual.IsValid() { t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) } else if !expected.IsValid() && actual.IsValid() { t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) } else if !expected.IsValid() && !actual.IsValid() { return } if expected.Type() != actual.Type() { t.Fatalf("%s: Expected %s type for %s, found %s\n", t.Name(), expected.Type(), fieldName, actual.Type()) } equalMethod := equalMethodOf(expected) if equalMethod.IsValid() { in := []reflect.Value{actual} out := equalMethod.Call(in) if !out[0].Bool() { t.Fatalf("%s: %s !Equal(): expected '%s', got '%s'\n", t.Name(), fieldName, valueToString(expected), valueToString(actual)) } return } switch expected.Kind() { case reflect.Array: for i := 0; i < expected.Len(); i++ { checkEqual(t, fmt.Sprintf("%s[%d]", fieldName, i), expected.Index(i), actual.Index(i)) } case reflect.Slice: if !expected.IsNil() && actual.IsNil() { t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) } else if expected.IsNil() && !actual.IsNil() { t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) } if expected.Len() != actual.Len() { t.Fatalf("%s: Expected len(%s) to to be %d, was %d\n", t.Name(), fieldName, expected.Len(), actual.Len()) } for i := 0; i < expected.Len(); i++ { checkEqual(t, fmt.Sprintf("%s[%d]", fieldName, i), expected.Index(i), actual.Index(i)) } case reflect.Interface: if !expected.IsNil() && actual.IsNil() { t.Fatalf("%s: %s was unexpectedly nil\n", t.Name(), fieldName) } else if expected.IsNil() && !actual.IsNil() { t.Fatalf("%s: Expected %s to be nil (it wasn't)\n", t.Name(), fieldName) } checkEqual(t, fieldName, expected.Elem(), actual.Elem()) case reflect.Ptr: checkEqual(t, fieldName, expected.Elem(), actual.Elem()) case reflect.Struct: structType := expected.Type() for i, n := 0, expected.NumField(); i < n; i++ { field := structType.Field(i) // skip XMLName fields so we can be lazy and not fill them out in // testing code var xmlname xml.Name if field.Name == "XMLName" && field.Type == reflect.TypeOf(xmlname) { continue } // Construct a new field name for this field, containing the parent // fieldName newFieldName := fieldName if fieldName != "" { newFieldName = fieldName + "." } newFieldName = newFieldName + field.Name checkEqual(t, newFieldName, expected.Field(i), actual.Field(i)) } case reflect.String: if expected.String() != actual.String() { t.Fatalf("%s: %s expected to be '%s', found '%s'\n", t.Name(), fieldName, expected.String(), actual.String()) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if expected.Uint() != actual.Uint() { t.Fatalf("%s: %s expected to be '%s', found '%s'\n", t.Name(), fieldName, valueToString(expected), valueToString(actual)) } default: t.Fatalf("%s: %s has unexpected type that didn't provide an Equal() method: %s\n", t.Name(), fieldName, expected.Type().Name()) } } func checkResponsesEqual(t *testing.T, expected, actual *Response) { 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) { fn := func(path string, info os.FileInfo, err error) error { if info.IsDir() { return nil } else if ext := filepath.Ext(path); ext != ".ofx" && ext != ".qfx" { return nil } file, err := os.Open(path) if err != nil { t.Fatalf("Unexpected error opening %s: %s\n", path, err) } response, err := ParseResponse(file) if err != nil { t.Fatalf("Unexpected error parsing OFX response in %s: %s\n", path, err) } checkResponseRoundTrip(t, response) return nil } 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 0 Info ENG 0 0 Info ` 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) } } ================================================ FILE: samples/busted_responses/bmo_v102__no_header_newline.qfx ================================================ OFXHEADER:100 DATA:OFXSGML VERSION:102 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 0 INFO OK 20181202184906.217[-5:EDT] SJLDF802DV09DF80 ENG 00017 1 0 INFO OK CAD 2380370270281083 20181202184905.909[-5:EDT] 20181202184905.909[-5:EDT] CREDIT 20181030000000.000[-5:EDT] 2042.24 2380370270281083201810302054456 PAYMENT RECEIVED - THANK YOU -552.63 20181202184906.217[-5:EDT] -552.63 20181202184906.217[-5:EDT] ================================================ FILE: samples/busted_responses/wellsfargo.qfx ================================================ OFXHEADER:100DATA:OFXSGMLVERSION:102SECURITY:NONEENCODING:USASCIICHARSET:1252COMPRESSION:NONEOLDFILEUID:NONENEWFILEUID:NONE0INFOSUCCESS20210102211014.201[-8:PST]ENGWF1000abc-1231000jane_doe00INFOSUCCESSUSD1234567899876543210CHECKING20201201120000.000[-8:PST]20201231120000.000[-8:PST]DIRECTDEBIT20201201120000.000[-8:PST]-12.34202012011AE Visa Card AE EPAY XXXXX1234123.4520201231120000.000[-8:PST]123.4520201231120000.000[-8:PST] ================================================ FILE: samples/valid_responses/401k_v203.ofx ================================================ 0INFOSUCCESS20170406202132.828[-4:EDT]ENGSAHWI8133 c8e9b92c-bf70-4145-9c24-c85284b20d3c 0INFOSUCCESS20170406192133.020[-4:EDT]USDfi.example.com79451753939 20170105192132.950[-5:EST]20170405202132.950[-4:EDT]4efe0ca7-0f02-40fa-a064-aba0613bd6bb20170106070000.000[-5:EST]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 01/06/2017OAELCUSIP1451.8174914388237-1511.65472536130913909.809828287014OTHEROTHERBUY c91ded6a-7587-4e12-9981-b41ad8a10b1120170120070000.000[-5:EST]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 01/20/2017OAELCUSIP1698.1335786250331727.67653039673183577.2910257455405OTHEROTHERBUY d4e8b94d-0601-450c-b862-e3eb4ab9bcf720170203070000.000[-5:EST]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 02/03/2017OAELCUSIP-861.01042108323374091.2289289557075258.62764651238285OTHEROTHERBUY ce27d094-83fb-47c4-9cc7-942b8fa90f2a20170217070000.000[-5:EST]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 02/17/2017OAELCUSIP1483.6339601876894962.565904371854894.689514323327OTHEROTHERBUY 2c678c32-bdb2-411c-95be-ce4b0b22476c20170303070000.000[-5:EST]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 03/03/2017OAELCUSIP4820.2639070207514533.5080512535124586.446085462384OTHEROTHERBUY 87f95061-6eb8-4417-872d-b6b50d59b82820170317070000.000[-4:EDT]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 03/17/2017OAELCUSIP398.121303592846744462.870483725029-980.7454839221953OTHEROTHERBUY 8d82af65-1ec2-4617-a58e-66f9f1f17a1b20170331070000.000[-4:EDT]CONTRIBUTION;VANGUARD TARGET 2045 OAEL;as of 03/31/2017OAELCUSIP-1675.0475911448093398.7552753025414137.105758433049OTHEROTHERBUY OAELCUSIPOTHERLONG3508.70724310179874265.298363227409-1285.6893734826635 20170405160000.000[-4:EDT] Market close as of 04/05/2017;VANGUARD TARGET 2045 311823602408MarketValueMarketValueDOLLAR238.618275091525920170406192133.020[-4:EDT]VestedValueVestedValueDOLLAR4769.86211757479720170406192133.020[-4:EDT]TotalAssetsValueTotalAssetsValueDOLLAR3159.394309476354520170406192133.020[-4:EDT]QC 401(K) PLAN2951.191737953516MarketValueMarketValueDOLLAR1855.5655765219920170406192133.020[-4:EDT]VestedValueVestedValueDOLLAR1535.829894444020720170406192133.020[-4:EDT]TotalAssetsValueTotalAssetsValueDOLLAR62.490406337568420170406192133.020[-4:EDT] OAELCUSIPVANGUARD TARGET 2045OAEL-281.337162358610420170405160000.000[-4:EDT]Market close as of 04/05/2017;VANGUARD TARGET 2045OTHER ================================================ FILE: samples/valid_responses/inv_v202.ofx ================================================ 0INFOSuccessful Sign On20170406192303[-5:EST]ENG20140605083000TLILW105506NCJ.wUGTRH0oti9WlMhf3pe_rqXxd1ukz2dd4ac034-448a-4613-b91d-984928345d6c0INFO20170406160000.000[-5:EST]USDfi.example.com1397128891820151006160000.000[-5:EST]160000.000[-5:EST]20170406192303.000[-5:EST]de647ad6-38fb-431e-aac4-34c5f13c420e20170315160000.000[-5:EST]20170316160000.000[-5:EST]BUY921937108CUSIP3824.95614977769862859.475796083826-943.5043319408105CASHCASHBUY714ccb86-c5a7-4c9d-9cf9-8f32ef1e679f20160606160000.000[-5:EST]20160607160000.000[-5:EST]BUY922908728CUSIP-936.6718270646302-1111.93066747011931881.084138894339CASHCASHBUY4cb6dcae-cb86-4344-8570-a9c5ec9708d720160708160000.000[-5:EST]20160711160000.000[-5:EST]BUY922908728CUSIP699.4447046891851363.80277925699653795.512144054378CASHCASHBUYefaddcc4-cede-4bef-b9ef-7da02d544a3a20160624160000.000[-5:EST]20160627160000.000[-5:EST]BUY921909602CUSIP-494.0111590621729649.39274242653801261.55338688051506CASHCASHBUYa21af8fb-6054-439c-9a26-2e14ba93416020160708160000.000[-5:EST]20160711160000.000[-5:EST]BUY921909602CUSIP3736.38950697004663698.7829383158605-1897.0973450115864CASHCASHBUY8de3b4f5-ad22-4805-8fd1-f72344e8630020160322160000.000[-5:EST]20160322160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP3221.5665716980166705.5007864208351-1322.9060478923284CASHCASHBUYad28b781-8372-416a-9755-61e2d44489be20160331160000.000[-5:EST]20160331160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP3674.003918319091-1435.673365926304-362.7565458501381CASHCASHBUY32993fde-e2db-4f79-8d4c-dcc49cdcb72c20160412160000.000[-5:EST]20160412160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP-495.00661745345815-1274.74173044648681551.8392618390644CASHCASHBUY8e5edf9c-8c21-4501-ab59-4415cf4170f620160623160000.000[-5:EST]20160623160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP3515.9628441137-1787.7474642873335-1818.8440342775998CASHCASHBUYb4d01c4c-e1ef-409d-9f48-2b644d9d848f20160629160000.000[-5:EST]20160629160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP1225.4461429600337366.51659363325551153.2510138507491CASHCASHBUYeec03d0c-6453-4a53-b0a8-51f702e949aa20160713160000.000[-5:EST]20160713160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP2498.406083177002-1208.30101628221151600.132544574224CASHCASHBUY502d83b5-ffa8-48bb-934c-56325c48a81f20160729160000.000[-5:EST]20160729160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP4993.9239541288221707.53317966301983649.7280516953333CASHCASHBUY5cafa134-09d3-4b3f-a1f1-025cd8adbae520160620160000.000[-5:EST]20160623160000.000[-5:EST]BUY921909768CUSIP797.57657718188472662.72040200787-275.77051479619445CASHCASHBUYe81bfeb2-499e-4f99-bbf6-79d53f20147d20160407160000.000[-5:EST]20160412160000.000[-5:EST]BUY922908769CUSIP970.9179101188292-1998.7249049199902-164.63123293080207CASHCASHBUYe8be8c30-ebe5-4569-b28e-fb70879c1ccd20160620160000.000[-5:EST]20160623160000.000[-5:EST]BUY921937835CUSIP3873.4976185528733512.50937639199133108.422217648994CASHCASHBUYb9d4de3a-4adc-4a8f-a105-65309d891aec20160729160000.000[-5:EST]20160729160000.000[-5:EST]DIVIDEND PAYMENTDIVIDEND PAYMENT78462F103CUSIPDIV-1477.6134874063105CASHCASHb9bb3cd8-def2-4d56-802e-ec568e59d52820160627160000.000[-5:EST]20160627160000.000[-5:EST]DIVIDEND PAYMENTDIVIDEND PAYMENTFED12Q45QCUSIPDIV1751.5484207314612CASHCASH68dd7706-2643-4536-a393-559358d41f3b20160606160000.000[-5:EST]20160606160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT049560105CUSIPDIV4665.123209260046CASH687.40620741189972608.9777289608946767d3192-8adb-432d-a497-d8a8194976bd20160906160000.000[-5:EST]20160906160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT049560105CUSIPDIV3437.8395682876217CASH3896.0034382169033281.585047888550cad8534-c48a-4163-919f-30a4d3387bc720161212160000.000[-5:EST]20161212160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT049560105CUSIPDIV559.222484741992CASH1000.9104415847364967.56716636651904ea0cd9-d24d-4c4f-bf83-d292e0cc7e7020170313160000.000[-5:EST]20170313160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT049560105CUSIPDIV-804.5795400324632CASH3460.5304681102464531.6556279865291715c17f-4196-4869-ae4c-74bfda77cdf620170331160000.000[-5:EST]20170331160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937108CUSIPDIV-199.0248089881802CASH-896.93731786859213769.556536805486510c52d3-5a36-4a49-bb89-d27a88c3889620160429160000.000[-5:EST]20160429160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT78462F103CUSIPDIV3599.617072348834CASH4217.2477259365614443.3074231888171bbda50a-4241-4574-8eea-6d701915eaa620160620160000.000[-5:EST]20160620160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908769CUSIPDIV4801.656470737234CASH4850.350814371729197.0213835112390415f48a59-fa18-4d79-be7b-aa895dace10e20160613160000.000[-5:EST]20160613160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV4283.192496197073CASH50.3849084108860554191.032455604349c0cbe4af-a9cc-414f-a2f3-1c16c7e8ce0920160912160000.000[-5:EST]20160912160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV2592.639549298905CASH3013.4291202195436-1532.838638652374896c8ba63-b67d-4c01-97dc-4f0197da4e5520161219160000.000[-5:EST]20161219160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV-840.5784945499015CASH1266.3124197606183726.484685708977bb0c2a67-2997-401d-9fec-75ac7da0e25a20170323160000.000[-5:EST]20170323160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV194.4819761372787CASH569.94903609035014927.7351964763226633eb88-d7be-4852-a253-950b2195a8be20160708160000.000[-5:EST]20160708160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-1888.2662493974499CASH1478.387428924409-99.46010783475981a5e85d6a-cbe9-4eb1-a8ca-2f2963610f9c20160805160000.000[-5:EST]20160805160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-1386.5211858727876CASH1508.1581641848293-1378.7609375925254878c9aba-7661-4b62-a6d3-cf87971845f220160908160000.000[-5:EST]20160908160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-1414.0447828528177CASH445.5239214525827-172.456354594676448e41a7e1-d66a-428f-9cda-88faa50f4c1120161007160000.000[-5:EST]20161007160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV4836.862627205668CASH-1848.73982412405533484.70494148761fd5193a1-50ff-4c77-992b-f121fe18ac9d20161107160000.000[-5:EST]20161107160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV1674.3790257730475CASH4068.3057004416214-458.0712334291586246eefac-86a5-48d9-ac62-88665d4f57cb20161207160000.000[-5:EST]20161207160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-697.60213411001CASH1223.1913821397843-821.7273826433402ff06f5a6-4806-403b-8091-743dbbe3d66320161229160000.000[-5:EST]20161229160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV3770.210174013371CASH1489.7888313470862815.1762795992504df123aa5-b2c1-48b7-a41e-d475005fc9ca20161229160000.000[-5:EST]20161229160000.000[-5:EST]DIV REINVEST LT CAP GAIN921937835CUSIPDIV1781.9847395203892CASH-1864.7526784443658860.6009700187328db167683-8917-4f21-862a-b2170aebd0ac20161229160000.000[-5:EST]20161229160000.000[-5:EST]DIV REINVEST ST CAP GAIN921937835CUSIPDIV2851.6793885008674CASH429.0849160317348-1222.3552596775967158986ca-ab01-4edf-9312-094c2d1e402520170207160000.000[-5:EST]20170207160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV663.9823123641668CASH4899.138916697189-1563.802650539355b0925442-08c3-4c67-ad18-ea6d1dd04f2f20170307160000.000[-5:EST]20170307160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV2744.502124012006CASH1958.14772993497444132.2282028408147df019fd-2285-4b05-88b6-f8ca2a34359420160330160000.000[-5:EST]20160330160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT464287655CUSIPDIV-1876.9501097583257CASH1112.6152006675607-1613.8151036363959ccc1740a-51c8-4a5c-8a5c-46e4068c92d320160912160000.000[-5:EST]20160912160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909602CUSIPDIV-747.5029507231845CASH-427.03654986838114-833.4083610380633ced017e7-18d8-4fa1-acb4-a0d19f346f6e20161219160000.000[-5:EST]20161219160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909602CUSIPDIV4261.009665361342CASH-388.70657313126662042.101658644944aebcf801-43cf-45bd-91ed-e6400c30fa8320170323160000.000[-5:EST]20170323160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909602CUSIPDIV2465.891796916825CASH4356.4145624090015-970.85511664226448062028a-05cb-4b0e-992a-a5bea0cfaddd20160331160000.000[-5:EST]20160331160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV258.71226421087067CASH1994.45032228582893550.717727066314db1db72d-9694-42bf-a03c-22e7fed8e13a20160429160000.000[-5:EST]20160429160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV3688.672913300633CASH3651.4810695887454933.925977518092209955123-3065-4076-880f-601ccc95f58a20160531160000.000[-5:EST]20160531160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV2666.517573929893CASH-347.17198994326554-1311.963332976477a5767318-b4eb-4ffd-a7a3-2cbed4858ea820160831160000.000[-5:EST]20160831160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV4951.177346049531CASH4175.989618842068-1358.888870337600673678cfe-03c9-4cdd-ba68-0faca0d0e25420160930160000.000[-5:EST]20160930160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV-443.4906622325843CASH-1435.7484320772205275.572309265066454c5cbd18-46c0-4202-9396-e61cbca47b8d20161031160000.000[-5:EST]20161031160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV1605.7657503064056CASH3971.830099850472-895.7166526891983b9c97d11-473b-4ebd-bf8d-2acfd697cdba20161130160000.000[-5:EST]20161130160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV2324.900790886096CASH-458.5953248656965-917.860405093177617ccd8b3-53a2-4263-bfcb-362f65a0cd2420161230160000.000[-5:EST]20161230160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV-788.0354705690729CASH3433.538315146986721.2715150817912f8990096-bb99-4436-872f-b201748a658e20170131160000.000[-5:EST]20170131160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV3694.493368489051CASH2123.633147710315-1842.162661124652c723a4c9-38a5-4177-bb54-27f6d649714820170228160000.000[-5:EST]20170228160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV-1686.0597755682554CASH-426.95493832616444-1490.57514769383837c196cd5-b15c-4c28-97d6-662c73de39f120170331160000.000[-5:EST]20170331160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV1608.9423884884068CASH2806.670702862508-1985.018659791540183d5c032-b138-4e04-81d3-cd28ae0fb24820160329160000.000[-5:EST]20160329160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT78464A763CUSIPDIV4822.357184455312CASH3187.176159151365229.30250007374025c7c27965-0509-4a2e-80ee-d4fe829f6e0320160607160000.000[-5:EST]20160607160000.000[-5:EST]MONEY FUND REDEMPTIONFED12Q45QCUSIP3020.49801798118374057.2553797251203-602.8925974222395CASHCASHSELLd4c6df49-3d9b-43b8-aedd-3caab2aff6ec20160627160000.000[-5:EST]20160627160000.000[-5:EST]MONEY FUND REDEMPTIONFED12Q45QCUSIP3130.1288139552935-1223.50510729259054764.349342218469CASHCASHSELLddd1cb7d-0404-4355-a62d-3c817621695520160711160000.000[-5:EST]20160711160000.000[-5:EST]MONEY FUND REDEMPTIONFED12Q45QCUSIP3009.6917633415141428.74646829681983738.2011235349337CASHCASHSELLc68a0eac-e703-438a-b47c-42f70382dbeb20160708160000.000[-5:EST]20160713160000.000[-5:EST]SELL921909768CUSIP651.04227006803191494.67656001636672026.13941934659261945.5385333219733CASHCASHSELLbaa02c1e-6301-46ff-ba04-e1151bea8c2e20160620160000.000[-5:EST]20160623160000.000[-5:EST]SELL084670702CUSIP-1263.5789517595033-1328.9207615255718-599.6197666896526-870.6754395021421-464.9649670580118CASHCASHSELL410f4e07-cf3e-46cf-a006-3cb0e0fa6f8a20160624160000.000[-5:EST]20160629160000.000[-5:EST]SELL049560105CUSIP-1347.12626913254373782.70015101654832306.0165284196121615.22028259357153318.618515196583CASHCASHSELLb4824f9e-0bbc-4d51-b685-e06677998a9020160620160000.000[-5:EST]20160623160000.000[-5:EST]SELL78462F103CUSIP3140.7458540383853531.6484043974051906.59365435979132449.2303779168496687.3186255123292CASHCASHSELL96b78949-4287-45e7-9fa4-5ab928f1708d20160708160000.000[-5:EST]20160713160000.000[-5:EST]SELL922908769CUSIP-572.4558678467724860.7465602131565-1428.2788946004944027.049420348547CASHCASHSELL19d3b807-2857-4805-9465-d74140a92c4920160620160000.000[-5:EST]20160623160000.000[-5:EST]SELL464287655CUSIP3812.8685527163107-1887.2736512639467-1941.16003248683273342.565983781469-152.64856008477113CASHCASHSELL8cb56b3e-cb37-4c1d-9389-45568e4ddc5d20160407160000.000[-5:EST]20160412160000.000[-5:EST]SELL78464A763CUSIP-767.06414146225531903.9549188232952900.987838172772-19.953350790284958-1724.3355702286553CASHCASHSELL272deeb3-b738-43f4-b767-d98b9b95332b20160321160000.000[-5:EST]20160321160000.000[-5:EST]TRANSFER OF ACCOUNTTRANSFER OF ACCOUNT084670702CUSIPCASH1810.3258411454908INLONG5ad31721-4669-4e28-99f5-8c0c709847d320160321160000.000[-5:EST]20160321160000.000[-5:EST]TRANSFER OF ACCOUNTTRANSFER OF ACCOUNT049560105CUSIPCASH1792.0252683912558INLONG14b2b7cb-52d5-4326-8bae-fa39ef94c1f320160321160000.000[-5:EST]20160321160000.000[-5:EST]TRANSFER OF ACCOUNTTRANSFER OF ACCOUNT78462F103CUSIPCASH696.0835085032941INLONGed56c305-3a72-4402-943f-3bed81cf21a720160321160000.000[-5:EST]20160321160000.000[-5:EST]TRANSFER OF ACCOUNTTRANSFER OF ACCOUNT464287655CUSIPCASH-1602.927207913195INLONG057cf934-14e7-41d5-bbe2-27f980d9f7fa20160321160000.000[-5:EST]20160321160000.000[-5:EST]TRANSFER OF ACCOUNTTRANSFER OF ACCOUNT78464A763CUSIPCASH2905.7461268824436INLONGOTHER20160321160000.000[-5:EST]-64.40574934902884248704e4-687d-4062-aa54-f0dd8bb8def4CASHTRANSFER OF ACCOUNTTRANSFER OF ACCOUNTCASHOTHER20160330160000.000[-5:EST]1712.055473010943467e92ad1-eb71-4c20-945d-34fb05c7b57fCASHTRANSFER OF ACCOUNTTRANSFER OF ACCOUNTCASHOTHER20160607160000.000[-5:EST]-1991.15157609478187bcae2a6-28c4-46fa-a2df-45d70951de40CASHOTHER20170316160000.000[-5:EST]1610.06558203369978748c137-21b1-4300-9fd7-4ab435ca0e15CASH049560105CUSIPCASHLONG3095.2062305114631645.5817152750637-376.594395703613420170405160000.000[-5:EST]Price as of date based on closing priceY921909602CUSIPCASHLONG642.90183396273781109.30604324273371904.519017199667420170405160000.000[-5:EST]Price as of date based on closing priceYY921937108CUSIPCASHLONG195.516861931936554982.684391844139-1296.703192780237720170405160000.000[-5:EST]Price as of date based on closing priceYY921937835CUSIPCASHLONG544.8705218820041-1203.8947910486945-623.057181688787520170405160000.000[-5:EST]Price as of date based on closing priceYN922908728CUSIPCASHLONG1586.1921654453394143.4341398281622460.968553438450720170405160000.000[-5:EST]Price as of date based on closing priceYY922906300CUSIPCASHLONG80.78210736327719-1355.24642362525673770.789918083696620170405160000.000[-5:EST]Price as of date based on closing priceNN-1525.5718103575014-136.379982987127734487.050086486819049560105CUSIPATMOS ENERGY CORPATO44.124154010998836Price as of date based on closing priceCOMMON-1942.3433104810056921909768CUSIPVANGUARD TOTAL INTL STOCK INDE921909768BUY084670702CUSIPBERKSHIRE HATHAWAY INC DE CL B084670702TRANSFER OF ACCOUNT78462F103CUSIP20SPDR SP 500 ETF78462F103TRANSFER OF ACCOUNT922908769CUSIPVANGUARD TOTAL STOCK MARKET ETF922908769BUY464287655CUSIPISHARES RUSSELL 2000 ETF464287655TRANSFER OF ACCOUNT78464A763CUSIP20SPDR SERIES TRUST SP DIVIDEND78464A763TRANSFER OF ACCOUNT921909602CUSIPVanguard Total International Stock Index Fund Investor SharesVGTSX-970.1670162560076Price as of date based on closing priceOPENEND921937108CUSIPVanguard Total Bond Market Index Fund Investor SharesVBMFX-687.4978500583932Price as of date based on closing priceOPENEND921937835CUSIPVANGUARD TOTAL BOND MARKET ETFBND4236.284682348263Price as of date based on closing priceOPENEND922908728CUSIPVanguard Total Stock Market Index Fund Admiral SharesVTSAX-1555.4850628829035Price as of date based on closing priceOPENEND922906300CUSIPVanguard Federal Money Market FundVMFXX2587.989630805755Price as of date based on closing priceOPENENDFED12Q45QCUSIPVANGUARD FEDERAL MONEY MARKETFED12Q45QMONEY FUND PURCHASEOPENEND ================================================ FILE: samples/valid_responses/ira_v202.ofx ================================================ 0INFOSuccessful Sign On20170406192636[-5:EST]ENG20140605083000OAHGK3418J3stupv8Hc1_S5omeZDXUhGx4P7QiAqdIlEgeaa98f87-e8f9-43e8-bacf-34d171f25fc90INFO20170406160000.000[-5:EST]USDfi.example.com3814078791920151006160000.000[-5:EST]160000.000[-5:EST]20170406192636.000[-5:EST]73ef3a78-8826-4255-ad94-5fd3d02d0ee520160216160000.000[-5:EST]20160216160000.000[-5:EST]MONEY FUND PURCHASE922906201CUSIP19.278217566280546-1525.46637119377784363.22682198809CASHCASHBUY59febec8-0659-4485-ae49-ba7c96700a7a20160623160000.000[-5:EST]20160623160000.000[-5:EST]MONEY FUND PURCHASE922906201CUSIP4647.8614059615692278.3676946161662469.6389083029935CASHCASHBUY22bf0c32-1b0e-40d4-9ca6-ad09e1334f8c20160919160000.000[-5:EST]20160919160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP1631.66991599555471634.40646443816511449.4689262297838CASHCASHBUY85cccd7d-a1d2-42c5-9a56-303d32bfb4c020160920160000.000[-5:EST]20160920160000.000[-5:EST]MONEY FUND PURCHASEFED12Q45QCUSIP-1170.59114815825434914.434618559914623.239143201663CASHCASHBUY1266686a-3499-479f-840b-c83046d1cbb420160915160000.000[-5:EST]20160916160000.000[-5:EST]BUY922906201CUSIP1879.6402178171297-1749.22695244310631673.3291030640453CASHCASHBUY2246810d-61ef-48eb-92da-9daab8a6730420160211160000.000[-5:EST]20160212160000.000[-5:EST]BUY922908728CUSIP3671.138197686205-845.2352332259197271.334148396772CASHCASHBUYc337e361-1814-4f5f-adb6-cfb7c4fa972620160304160000.000[-5:EST]20160309160000.000[-5:EST]BUY921909768CUSIP429.2321735536443-877.5569867259219-114.01187925332829CASHCASHBUY36e24251-84ac-4417-af61-d5d744e7abf020160620160000.000[-5:EST]20160623160000.000[-5:EST]BUY921909768CUSIP1826.59889197070654168.070526154769215.5740907298882CASHCASHBUY7af54cfd-9c78-4a76-b22d-2033224da06f20160916160000.000[-5:EST]20160916160000.000[-5:EST]DIVIDEND PAYMENTDIVIDEND PAYMENT922906201CUSIPDIV4761.745483958354CASHCASHd453519b-1c71-4e15-9809-89f42f5cb62820160229160000.000[-5:EST]20160229160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV2231.878532889339CASH4449.7304438202373969.37915587410235f08d9e2-d001-4921-9449-4132e8e13ec020160331160000.000[-5:EST]20160331160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV-410.30842159812414CASH-1959.5556604318092-219.51461198200673ce0d9325-fbb4-44bf-bff7-dcacc53b3e9920160429160000.000[-5:EST]20160429160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV-1486.5386646073728CASH3773.739320660916-1746.90273685246783a727ea0-122b-4cb4-97ab-2576d23e700c20160531160000.000[-5:EST]20160531160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV2534.453837971075CASH-393.44253321583165252.97625493777514ddba8b2d-ddde-48c8-ba29-a235a93057fc20160630160000.000[-5:EST]20160630160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV376.163078287369CASH1556.8542525319134125.2718950706540700e49f-7ba5-4b3a-a691-7c6d13a092eb20160729160000.000[-5:EST]20160729160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV3989.128366075679CASH4865.9429553316872917.8660022095837c59073eb-b79a-4e5f-9431-ecc8a55f22c620160831160000.000[-5:EST]20160831160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922906201CUSIPDIV4078.3093503085493CASH-496.184340784390772753.14048291157117c5fe14-84a3-4a73-af59-bf30b548cb6a20160321160000.000[-5:EST]20160321160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909768CUSIPDIV-1221.9538935107803CASH4746.528489740064959.2180222934749ad51c9bd-8f75-43e5-840f-a70cd26a24dd20160620160000.000[-5:EST]20160620160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909768CUSIPDIV-1857.2140850765047CASH3806.2627930727833235.6283373172528136b035b-f86b-4675-9c6b-4ae0d94a0e0e20160919160000.000[-5:EST]20160919160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909768CUSIPDIV3312.468204221862CASH2275.72729872343832929.3284555223727688dc78f-fd4d-4e32-b2b8-0f617c586dd520161227160000.000[-5:EST]20161227160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909768CUSIPDIV3035.100981165604CASH2419.275917573039-1467.6568132937696a3741898-1c78-4ef0-af4a-6b8257d621d920170330160000.000[-5:EST]20170330160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921909768CUSIPDIV2551.8931819614763CASH302.28999602868134-879.0269579585956cb9d79b9-95bd-48fc-96f8-994a53629a2b20161031160000.000[-5:EST]20161031160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV1148.6433443100404CASH4252.1044936154253234.07741416937e051e4ec-8270-4ca6-93c3-04c27b922a2420161130160000.000[-5:EST]20161130160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV2508.700799816961CASH2766.59908465249743605.4590902283144015a3fd7-e0e7-435d-b855-34ab8a227e1e20161230160000.000[-5:EST]20161230160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV483.7378756418202CASH3472.482451765719590.102890104742538a4ec6a-38e1-4212-b8e6-3bb8c8178e9e20170131160000.000[-5:EST]20170131160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV4427.3291160400495CASH14.790104918561838-442.8826055075442b49f4795-5d30-4e1c-af65-6cfe40ae549320170228160000.000[-5:EST]20170228160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV3029.2980480118968CASH2856.745750190623809.4489376295987270a9dc3-48e2-4a9f-a930-cd29e32b0d0020170331160000.000[-5:EST]20170331160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENTFED12Q45QCUSIPDIV2645.102484563339CASH-1673.34751196842174523.535003881921d79e8743-7954-4cab-a528-a8e06d3a0dd520160314160000.000[-5:EST]20160314160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV282.4616376483614CASH4587.87747813208298.54334236670365863cac2d-7fd2-4315-8a39-2f1254f796a320160613160000.000[-5:EST]20160613160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV916.914663767484CASH-14.358763846982811571.93968247551221fe41d06-0f89-40f0-b12d-311ded76d45f20160912160000.000[-5:EST]20160912160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV3431.2319557327646CASH37.208425084017113414.1136928125725ea033881-5ac6-4f95-8fe7-2c6c1a7d41f620161219160000.000[-5:EST]20161219160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV-431.16553302783905CASH-1160.4641462740697-1577.11934687287231b435119-4e87-4d97-bd5f-e44340d9389f20170323160000.000[-5:EST]20170323160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT922908728CUSIPDIV-1161.8607928529427CASH3671.4006888211971422.24454643582484f4ba849-7194-402f-8c92-8b761815995420160205160000.000[-5:EST]20160205160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-347.64977589301CASH2374.894731116042-1230.0935445193008c30bcf33-8edd-4dd7-91bf-d54b6eca97b720160307160000.000[-5:EST]20160307160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV2417.051313778199CASH461.9607296369909-1025.0001320219317324261d5-494c-49c4-bcfb-512f27d3702c20160407160000.000[-5:EST]20160407160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV3500.224708480051CASH2702.8310894059196-1792.26166194549328ac3202-1f40-4d26-9673-96261abf06a420160506160000.000[-5:EST]20160506160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-152.25279799932332CASH-501.217439303663-1264.293386130012fbd84e50-79ab-4093-8520-56c437c5989b20160607160000.000[-5:EST]20160607160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV2208.543518179934CASH-530.60294515534131272.880155297221399186239-b9c7-41de-a60b-f90ee4b261d820160708160000.000[-5:EST]20160708160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-277.9538080741984CASH-528.3679548856133868.1809909164426ca39969-5ba1-448d-9d73-d051e416333720160805160000.000[-5:EST]20160805160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV1390.1352177192844CASH-1282.7634111873078-1029.886744521014981191b4-40b0-41f8-8ba8-9c2063fe0ec920160908160000.000[-5:EST]20160908160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV782.6003016009354CASH1456.80474973333281249.82907632477969b7a3810-a11b-4b6f-911a-49e445b0018f20161007160000.000[-5:EST]20161007160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV70.3205044010142CASH3796.940584853094-1062.4570784720810766a122-24dc-4399-8123-c71115f6622920161107160000.000[-5:EST]20161107160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV2299.071174809411CASH4735.83145139016054788.54079641870232571a95-af17-4606-a711-a6dcf029b10a20161207160000.000[-5:EST]20161207160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-1533.0306472468815CASH-390.4765137990337459.8084925752209522e8ade5-d07a-448b-b406-78b45f6326cf20161229160000.000[-5:EST]20161229160000.000[-5:EST]DIV REINVEST LT CAP GAIN921937835CUSIPDIV1988.2260617644365CASH3758.62804215810054972.582895566067918a1c7-9ed0-4d04-a94c-065bf8b977f320161229160000.000[-5:EST]20161229160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-77.40659410599346CASH411.70609714231843148.7787880383175ebd51218-a3f3-47b9-9537-f63bd46bd30320161229160000.000[-5:EST]20161229160000.000[-5:EST]DIV REINVEST ST CAP GAIN921937835CUSIPDIV4773.43800249682CASH-1370.5195304265344-1062.96787945674491d921e57-baa4-4062-921a-fdbb9ab1f47a20170207160000.000[-5:EST]20170207160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV-1372.6835513390852CASH4065.6167094485336174.34112807874317c40d8462-85c1-4f77-b777-8f2744c34f9220170307160000.000[-5:EST]20170307160000.000[-5:EST]DIVIDEND REINVESTMENTDIVIDEND REINVESTMENT921937835CUSIPDIV2594.475521583572CASH-1651.70275877586054149.45666467826236d00a6f-6602-4593-a200-b723856ce62320160309160000.000[-5:EST]20160309160000.000[-5:EST]MONEY FUND REDEMPTION922906201CUSIP2334.814219650776-1694.56817710621144735.917005707134CASHCASHSELL925feb14-77d3-420a-827a-db93b4465af520160916160000.000[-5:EST]20160916160000.000[-5:EST]MONEY FUND REDEMPTION922906201CUSIP4295.3723644054553246.89490110866254834.973735025837CASHCASHSELLe5e66514-a870-4297-873a-6574d8a8b0aa20160210160000.000[-5:EST]20160216160000.000[-5:EST]SELL261978811CUSIP385.793787537458231754.8233843951994-1157.1992982444128527.6610260161524CASHCASHSELL9767f231-8dae-49fe-a005-d83f39fb571c20160919160000.000[-5:EST]20160920160000.000[-5:EST]SELL922906201CUSIP4378.1114214831862772.217534654248-1794.720488184712CASHCASHSELLba99872b-0a43-4e98-ad43-83676f13ccc820160620160000.000[-5:EST]20160623160000.000[-5:EST]SELL084670702CUSIP-714.1261345131281-342.09170872180543078.2330073727353-1464.49085573198844425.271509313134CASHCASHSELL05da01b5-a0c0-4bc7-8ebc-75045f0ddcb020160304160000.000[-5:EST]20160309160000.000[-5:EST]SELL921937835CUSIP34.998398887997612061.706825519594886.1447439244439-1552.353494167612CASHCASHSELL4183e7d7-a876-4435-9c4b-c87bbbf4526920160210160000.000[-5:EST]20160216160000.000[-5:EST]SELL464287465CUSIP3156.4568246974277-360.265783758550474017.01014113040044741.679805766312757.176333763726CASHCASHSELLe3579f9a-c7d0-4a6e-abcd-8ab6f049a1d520160202160000.000[-5:EST]20160202160000.000[-5:EST]FOR IRA ACCOUNTSFOR IRA ACCOUNTS261978811CUSIPCASH-944.6652942505041INLONGf7bed88a-9d81-405f-9c4b-6de2f6e4738220160202160000.000[-5:EST]20160202160000.000[-5:EST]FOR IRA ACCOUNTSFOR IRA ACCOUNTS084670702CUSIPCASH4492.551705542272INLONG64250efe-6a33-416d-a3ef-88eeb7a4d3f720160202160000.000[-5:EST]20160202160000.000[-5:EST]FOR IRA ACCOUNTSFOR IRA ACCOUNTS921937835CUSIPCASH-536.6639659620153INLONG6b2b601b-0c2c-4cfb-91e2-a268ecca703220160202160000.000[-5:EST]20160202160000.000[-5:EST]FOR IRA ACCOUNTSFOR IRA ACCOUNTS464287465CUSIPCASH2017.7913572287862INLONGOTHER20160202160000.000[-5:EST]3730.763383270170498ab64d2-3624-4e5f-abc5-b231993d3873CASHFOR IRA ACCOUNTSFOR IRA ACCOUNTSCASH921909768CUSIPCASHLONG1127.46110891858053421.9693338238414064.05755992575820170405160000.000[-5:EST]Price as of date based on closing priceYN921937835CUSIPCASHLONG3428.9852581709724183.637265410978245.477987580849920170405160000.000[-5:EST]Price as of date based on closing priceYN922908728CUSIPCASHLONG-743.0194609405111379.16074138536674216.26082300995120170405160000.000[-5:EST]Price as of date based on closing priceYY922906300CUSIPCASHLONG-1776.13078457013423758.02990034300274201.08747448789820170405160000.000[-5:EST]Price as of date based on closing priceNN3250.107711696731414.965453584748054842.585013190769084670702CUSIPBERKSHIRE HATHAWAY INC DE CL B084670702FOR IRA ACCOUNTS464287465CUSIPISHARES MSCI EAFE ETF464287465FOR IRA ACCOUNTS921909768CUSIPVANGUARD TOTAL INTL STOCK INDEX FUND ETFVXUS4231.006710834493Price as of date based on closing priceOPENEND921937835CUSIPVANGUARD TOTAL BOND MARKET ETFBND-1905.470808585281Price as of date based on closing priceOPENEND922908728CUSIPVanguard Total Stock Market Index Fund Admiral SharesVTSAX2130.136485094742Price as of date based on closing priceOPENEND922906300CUSIPVanguard Federal Money Market FundVMFXX1290.0386960392157Price as of date based on closing priceOPENEND922906201CUSIPVANGUARD PRIME MONEY MARKET FU922906201MONEY FUND PURCHASEOPENEND261978811CUSIP20DREYFUS / LAUREL INTL SP 500261978811FOR IRA ACCOUNTSOPENENDFED12Q45QCUSIPVANGUARD FEDERAL MONEY MARKETFED12Q45QMONEY FUND PURCHASEOPENEND ================================================ FILE: samples/valid_responses/moneymrkt1_v103.ofx ================================================ OFXHEADER:100 DATA:OFXSGML VERSION:103 SECURITY:NONE ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 0 INFO 20170407001840.607[0:GMT] ENG UJKDO 3534 e1707dfd-695d-4451-8d9c-0e142fdc456a 0 INFO USD 598813374 35342483513 MONEYMRKT 20170107011841.262[0:GMT] 20170407001841.262[0:GMT] CREDIT 20170117120000.000[0:GMT] -995.4190396554627 2fb2640c-cee3-4643-8ba3-ea21a4d18954 Dividend Earned CREDIT 20170215120000.000[0:GMT] 788.5385340523635 c9d856df-339c-47c6-9f6a-8c2e2910f62e Dividend Earned CREDIT 20170315120000.000[0:GMT] 3070.1328011762807 1107ace0-048b-4c0c-b5f3-45b6be4cd71d Dividend Earned 2607.1664944585727 20170407001841.262[0:GMT] 4503.683156768119 20170407001841.262[0:GMT] ================================================ FILE: samples/valid_responses/moneymrkt1_v103_TYPE1.ofx ================================================ OFXHEADER:100 DATA:OFXSGML VERSION:103 SECURITY:TYPE1 ENCODING:USASCII CHARSET:1252 COMPRESSION:NONE OLDFILEUID:NONE NEWFILEUID:NONE 0 INFO 20170407001840.607[0:GMT] ENG UJKDO 3534 e1707dfd-695d-4451-8d9c-0e142fdc456a 0 INFO USD 598813374 35342483513 MONEYMRKT 20170107011841.262[0:GMT] 20170407001841.262[0:GMT] CREDIT 20170117120000.000[0:GMT] -995.4190396554627 2fb2640c-cee3-4643-8ba3-ea21a4d18954 Dividend Earned CREDIT 20170215120000.000[0:GMT] 788.5385340523635 c9d856df-339c-47c6-9f6a-8c2e2910f62e Dividend Earned CREDIT 20170315120000.000[0:GMT] 3070.1328011762807 1107ace0-048b-4c0c-b5f3-45b6be4cd71d Dividend Earned 2607.1664944585727 20170407001841.262[0:GMT] 4503.683156768119 20170407001841.262[0:GMT] ================================================ FILE: samples/valid_responses/moneymrkt1_v203.ofx ================================================ 0 INFO 20170407001504.765[0:GMT] ENG EHPDK 4881 262e39f1-e698-48cb-b2a2-b2f8ac2478fa 0 INFO USD 188545178 83483499583 MONEYMRKT 20170107011505.296[0:GMT] 20170407001505.296[0:GMT] CREDIT 20170117120000.000[0:GMT] 1303.7355652284177 fa603343-dd05-434e-9ffc-933f51f8cedf Dividend Earned CREDIT 20170215120000.000[0:GMT] 4321.245210194117 061f8fb0-8b56-4938-a45e-5db0e42d74f7 Dividend Earned CREDIT 20170315120000.000[0:GMT] -850.1196618322863 a43dfadf-350b-4bb1-85d7-8be57ad0ef82 Dividend Earned 3317.738651126686 20170407001505.296[0:GMT] 658.9377225082694 20170407001505.296[0:GMT] ================================================ FILE: seclist.go ================================================ package ofxgo import ( "errors" "github.com/aclindsa/xml" ) // SecurityID identifies a security by its CUSIP (for US-based FI's, others may // use UniqueID types other than CUSIP) type SecurityID struct { XMLName xml.Name `xml:"SECID"` UniqueID String `xml:"UNIQUEID"` // CUSIP for US FI's UniqueIDType String `xml:"UNIQUEIDTYPE"` // Should always be "CUSIP" for US FI's } // SecurityRequest represents a request for one security. It is specified with // a SECID aggregate, a ticker symbol, or an FI assigned identifier (but no // more than one of them at a time) type SecurityRequest struct { XMLName xml.Name `xml:"SECRQ"` // Only one of the next three should be present SecID *SecurityID `xml:"SECID,omitempty"` Ticker String `xml:"TICKER,omitempty"` FiID String `xml:"FIID,omitempty"` } // SecListRequest represents a request for information (namely price) about one // or more securities type SecListRequest struct { XMLName xml.Name `xml:"SECLISTTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` Securities []SecurityRequest `xml:"SECLISTRQ>SECRQ,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (r *SecListRequest) Name() string { return "SECLISTTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *SecListRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement return true, nil } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *SecListRequest) Type() messageType { return SecListRq } // SecListResponse is always empty (except for the transaction UID, status, and // optional client cookie). Its presence signifies that the SecurityList (a // different element from this one) immediately after this element in // Response.SecList was been generated in response to the same SecListRequest // this is a response to. type SecListResponse struct { XMLName xml.Name `xml:"SECLISTTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` // SECLISTRS is always empty, so we don't parse it here. The actual securities list will be in a top-level element parallel to SECLISTTRNRS } // Name returns the name of the top-level transaction XML/SGML element func (r *SecListResponse) Name() string { return "SECLISTTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SecListResponse) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (r *SecListResponse) Type() messageType { return SecListRs } // Security is satisfied by all *Info elements providing information about // securities for SecurityList type Security interface { SecurityType() string SecurityInfo() SecInfo } // SecInfo represents the generic information about a security. It is included // in most other *Info elements. type SecInfo struct { XMLName xml.Name `xml:"SECINFO"` SecID SecurityID `xml:"SECID"` SecName String `xml:"SECNAME"` // Full name of security Ticker String `xml:"TICKER,omitempty"` // Ticker symbol FiID String `xml:"FIID,omitempty"` Rating String `xml:"RATING,omitempty"` UnitPrice Amount `xml:"UNITPRICE,omitempty"` // Current price, as of DTASOF DtAsOf *Date `xml:"DTASOF,omitempty"` // Date UNITPRICE was for Currency *Currency `xml:"CURRENCY,omitempty"` // Overriding currency for UNITPRICE Memo String `xml:"MEMO,omitempty"` } // DebtInfo provides information about a debt security type DebtInfo struct { XMLName xml.Name `xml:"DEBTINFO"` SecInfo SecInfo `xml:"SECINFO"` ParValue Amount `xml:"PARVALUE"` DebtType debtType `xml:"DEBTTYPE"` // One of COUPON, ZERO (zero coupon) DebtClass debtClass `xml:"DEBTCLASS,omitempty"` // One of TREASURY, MUNICIPAL, CORPORATE, OTHER CouponRate Amount `xml:"COUPONRT,omitempty"` // Bond coupon rate for next closest call date DtCoupon *Date `xml:"DTCOUPON,omitempty"` // Maturity date for next coupon CouponFreq couponFreq `xml:"COUPONFREQ,omitempty"` // When coupons mature - one of MONTHLY, QUARTERLY, SEMIANNUAL, ANNUAL, or OTHER CallPrice Amount `xml:"CALLPRICE,omitempty"` // Bond call price YieldToCall Amount `xml:"YIELDTOCALL,omitempty"` // Yield to next call DtCall *Date `xml:"DTCALL,omitempty"` // Next call date CallType callType `xml:"CALLTYPE,omitempt"` // Type of next call. One of CALL, PUT, PREFUND, MATURITY YieldToMat Amount `xml:"YIELDTOMAT,omitempty"` // Yield to maturity DtMat *Date `xml:"DTMAT,omitempty"` // Debt maturity date AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } // SecurityType returns a string representation of this security's type func (i DebtInfo) SecurityType() string { return "DEBTINFO" } // SecurityInfo returns SecInfo func (i DebtInfo) SecurityInfo() SecInfo { return i.SecInfo } // AssetPortion represents the percentage of a mutual fund with the given asset // classification type AssetPortion struct { XMLName xml.Name `xml:"PORTION"` AssetClass assetClass `xml:"ASSETCLASS"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class } // FiAssetPortion represents the percentage of a mutual fund with the given // FI-defined asset classification (AssetPortion should be used for all asset // classifications defined by the assetClass enum) type FiAssetPortion struct { XMLName xml.Name `xml:"FIPORTION"` FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class Percent Amount `xml:"PERCENT"` // Percentage of the fund that falls under this asset class } // MFInfo provides information about a mutual fund type MFInfo struct { XMLName xml.Name `xml:"MFINFO"` SecInfo SecInfo `xml:"SECINFO"` MfType mfType `xml:"MFTYPE"` // One of OPEN, END, CLOSEEND, OTHER Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price DtYieldAsOf *Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for AssetClasses []AssetPortion `xml:"MFASSETCLASS>PORTION"` FiAssetClasses []FiAssetPortion `xml:"FIMFASSETCLASS>FIPORTION"` } // SecurityType returns a string representation of this security's type func (i MFInfo) SecurityType() string { return "MFINFO" } // SecurityInfo returns SecInfo func (i MFInfo) SecurityInfo() SecInfo { return i.SecInfo } // OptInfo provides information about an option type OptInfo struct { XMLName xml.Name `xml:"OPTINFO"` SecInfo SecInfo `xml:"SECINFO"` OptType optType `xml:"OPTTYPE"` // One of PUT, CALL StrikePrice Amount `xml:"STRIKEPRICE"` DtExpire Date `xml:"DTEXPIRE"` // Expiration date ShPerCtrct Int `xml:"SHPERCTRCT"` // Shares per contract SecID *SecurityID `xml:"SECID,omitempty"` // Security ID of the underlying security AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } // SecurityType returns a string representation of this security's type func (i OptInfo) SecurityType() string { return "OPTINFO" } // SecurityInfo returns SecInfo func (i OptInfo) SecurityInfo() SecInfo { return i.SecInfo } // OtherInfo provides information about a security type not covered by the // other *Info elements type OtherInfo struct { XMLName xml.Name `xml:"OTHERINFO"` SecInfo SecInfo `xml:"SECINFO"` TypeDesc String `xml:"TYPEDESC,omitempty"` // Description of security type AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } // SecurityType returns a string representation of this security's type func (i OtherInfo) SecurityType() string { return "OTHERINFO" } // SecurityInfo returns SecInfo func (i OtherInfo) SecurityInfo() SecInfo { return i.SecInfo } // StockInfo provides information about a security type type StockInfo struct { XMLName xml.Name `xml:"STOCKINFO"` SecInfo SecInfo `xml:"SECINFO"` StockType stockType `xml:"STOCKTYPE,omitempty"` // One of COMMON, PREFERRED, CONVERTIBLE, OTHER Yield Amount `xml:"YIELD,omitempty"` // Current yield reported as the dividend expressed as a portion of the current stock price DtYieldAsOf *Date `xml:"DTYIELDASOF,omitempty"` // Date YIELD is valid for AssetClass assetClass `xml:"ASSETCLASS,omitempty"` // One of DOMESTICBOND, INTLBOND, LARGESTOCK, SMALLSTOCK, INTLSTOCK, MONEYMRKT, OTHER FiAssetClass String `xml:"FIASSETCLASS,omitempty"` // FI-defined asset class } // SecurityType returns a string representation of this security's type func (i StockInfo) SecurityType() string { return "STOCKINFO" } // SecurityInfo returns SecInfo func (i StockInfo) SecurityInfo() SecInfo { return i.SecInfo } // SecurityList is a container for Security objects containaing information // about securities type SecurityList struct { XMLName xml.Name `xml:"SECLIST"` Securities []Security } // Name returns the name of the top-level transaction XML/SGML element func (r *SecurityList) Name() string { return "SECLIST" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SecurityList) Valid(version ofxVersion) (bool, error) { // TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (r *SecurityList) Type() messageType { return SecListRs } // UnmarshalXML handles unmarshalling a SecurityList from an SGML/XML string func (r *SecurityList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { tok, err := nextNonWhitespaceToken(d) if err != nil { return err } else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local { // If we found the end of our starting element, we're done parsing return nil } else if startElement, ok := tok.(xml.StartElement); ok { switch startElement.Name.Local { case "DEBTINFO": var info DebtInfo if err := d.DecodeElement(&info, &startElement); err != nil { return err } r.Securities = append(r.Securities, Security(info)) case "MFINFO": var info MFInfo if err := d.DecodeElement(&info, &startElement); err != nil { return err } r.Securities = append(r.Securities, Security(info)) case "OPTINFO": var info OptInfo if err := d.DecodeElement(&info, &startElement); err != nil { return err } r.Securities = append(r.Securities, Security(info)) case "OTHERINFO": var info OtherInfo if err := d.DecodeElement(&info, &startElement); err != nil { return err } r.Securities = append(r.Securities, Security(info)) case "STOCKINFO": var info StockInfo if err := d.DecodeElement(&info, &startElement); err != nil { return err } r.Securities = append(r.Securities, Security(info)) default: return errors.New("Invalid SECLIST child tag: " + startElement.Name.Local) } } else { return errors.New("Didn't find an opening element") } } } // 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 } ================================================ FILE: signon.go ================================================ package ofxgo import ( "errors" "fmt" "github.com/aclindsa/xml" ) // SignonRequest identifies and authenticates a user to their FI and is // provided with every Request type SignonRequest struct { XMLName xml.Name `xml:"SONRQ"` DtClient Date `xml:"DTCLIENT"` // Current time on client, overwritten in Client.Request() UserID String `xml:"USERID"` UserPass String `xml:"USERPASS,omitempty"` UserKey String `xml:"USERKEY,omitempty"` GenUserKey Boolean `xml:"GENUSERKEY,omitempty"` Language String `xml:"LANGUAGE"` // Defaults to ENG Org String `xml:"FI>ORG"` Fid String `xml:"FI>FID"` AppID String `xml:"APPID"` // Overwritten in Client.Request() AppVer String `xml:"APPVER"` // Overwritten in Client.Request() ClientUID UID `xml:"CLIENTUID,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (r *SignonRequest) Name() string { return "SONRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *SignonRequest) Valid(version ofxVersion) (bool, error) { if len(r.UserID) < 1 || len(r.UserID) > 32 { return false, errors.New("SONRQ>USERID invalid length") } if (len(r.UserPass) == 0) == (len(r.UserKey) == 0) { return false, errors.New("One and only one of SONRQ>USERPASS and USERKEY must be supplied") } if len(r.UserPass) > 32 { return false, errors.New("SONRQ>USERPASS invalid length") } if len(r.UserKey) > 64 { return false, errors.New("SONRQ>USERKEY invalid length") } if len(r.Language) == 0 { r.Language = "ENG" } else if len(r.Language) != 3 { return false, fmt.Errorf("SONRQ>LANGUAGE invalid length: \"%s\"", r.Language) } if len(r.AppID) < 1 || len(r.AppID) > 5 { return false, errors.New("SONRQ>APPID invalid length") } if len(r.AppVer) < 1 || len(r.AppVer) > 4 { return false, errors.New("SONRQ>APPVER invalid length") } return true, nil } // SignonResponse is provided with every Response and indicates the success or // failure of the SignonRequest in the corresponding Request type SignonResponse struct { XMLName xml.Name `xml:"SONRS"` Status Status `xml:"STATUS"` DtServer Date `xml:"DTSERVER"` UserKey String `xml:"USERKEY,omitempty"` TsKeyExpire *Date `xml:"TSKEYEXPIRE,omitempty"` Language String `xml:"LANGUAGE"` DtProfUp *Date `xml:"DTPROFUP,omitempty"` DtAcctUp *Date `xml:"DTACCTUP,omitempty"` Org String `xml:"FI>ORG"` Fid String `xml:"FI>FID"` SessCookie String `xml:"SESSCOOKIE,omitempty"` AccessKey String `xml:"ACCESSKEY,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (r *SignonResponse) Name() string { return "SONRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (r *SignonResponse) Valid(version ofxVersion) (bool, error) { if len(r.Language) != 3 { return false, fmt.Errorf("SONRS>LANGUAGE invalid length: \"%s\"", r.Language) } return r.Status.Valid() } ================================================ FILE: signon_test.go ================================================ package ofxgo import ( "testing" ) func TestMarshalInvalidSignons(t *testing.T) { var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "myusername" request.Signon.UserPass = "Pa$$word" request.Signon.Org = "BNK" request.Signon.Fid = "1987" request.SetClientFields(&client) _, err := request.Marshal() if err != nil { t.Fatalf("Unexpected error marshalling signon: %s\n", err) } request.Signon.UserKey = "mykey" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to key and password both being specified\n") } request.Signon.UserPass = "" _, err = request.Marshal() if err != nil { t.Fatalf("Unexpected error marshalling signon: %s\n", err) } request.Signon.UserID = "" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to unspecified UserID\n") } request.Signon.UserID = "lakhgdlsakhgdlkahdglkhsadlkghaslkdghsalkdghalsdhg" if err == nil { t.Fatalf("Expected error due to UserID too long\n") } request.Signon.UserID = "myusername" request.Signon.UserKey = "adlfahdslkgahdweoihadf98agrha87rghasdf9hawhra2hrkwahhaguhwaoefajkei23hff" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to UserKey too long\n") } request.Signon.UserKey = "" request.Signon.UserPass = "adlfahdslkgahdweoihadf98agrha87rghasdf9hawhra2hrkwahhaguhwaoefajkei23hff" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to UserPass too long\n") } request.Signon.UserPass = "lakhgdlkahd" request.Signon.Language = "English" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to Language too long\n") } request.Signon.Language = "EN" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to Language too short\n") } request.Signon.Language = "" _, err = request.Marshal() if err != nil || request.Signon.Language != "ENG" { t.Fatalf("Empty Language expected to default to ENG: %s\n", err) } request.Signon.Language = "ENG" request.Signon.AppID = "" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to missing AppID\n") } request.SetClientFields(&client) _, err = request.Marshal() if err != nil { t.Fatalf("Client expected to set empty AppID: %s\n", err) } client.AppID = "ALKHGDH" request.SetClientFields(&client) _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to AppID too long\n") } client.AppID = "OFXGO" request.Signon.AppVer = "" _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to missing AppVer\n") } request.SetClientFields(&client) _, err = request.Marshal() if err != nil { t.Fatalf("Client expected to set empty AppVer: %s\n", err) } client.AppVer = "00002" request.SetClientFields(&client) _, err = request.Marshal() if err == nil { t.Fatalf("Expected error due to AppVer too long\n") } client.AppVer = "0001" request.SetClientFields(&client) _, err = request.Marshal() if err != nil { t.Fatalf("Unexpected error after resetting all fields to reasonable values: %s\n", err) } } ================================================ FILE: signup.go ================================================ package ofxgo import ( "fmt" "github.com/aclindsa/xml" ) // AcctInfoRequest represents a request for the server to provide information // for all of the user's available accounts at this FI type AcctInfoRequest struct { XMLName xml.Name `xml:"ACCTINFOTRNRQ"` TrnUID UID `xml:"TRNUID"` CltCookie String `xml:"CLTCOOKIE,omitempty"` TAN String `xml:"TAN,omitempty"` // Transaction authorization number // TODO `xml:"OFXEXTENSION,omitempty"` DtAcctUp Date `xml:"ACCTINFORQ>DTACCTUP"` } // Name returns the name of the top-level transaction XML/SGML element func (r *AcctInfoRequest) Name() string { return "ACCTINFOTRNRQ" } // Valid returns (true, nil) if this struct would be valid OFX if marshalled // into XML/SGML func (r *AcctInfoRequest) Valid(version ofxVersion) (bool, error) { if ok, err := r.TrnUID.Valid(); !ok { return false, err } // TODO implement return true, nil } // Type returns which message set this message belongs to (which Request // element of type []Message it should appended to) func (r *AcctInfoRequest) Type() messageType { return SignupRq } // HolderInfo contains the information a FI has about an account-holder type HolderInfo struct { XMLName xml.Name FirstName String `xml:"FIRSTNAME"` MiddleName String `xml:"MIDDLENAME,omitempty"` LastName String `xml:"LASTNAME"` Addr1 String `xml:"ADDR1"` Addr2 String `xml:"ADDR2,omitempty"` Addr3 String `xml:"ADDR3,omitempty"` City String `xml:"CITY"` State String `xml:"STATE"` PostalCode String `xml:"POSTALCODE"` Country String `xml:"COUNTRY,omitempty"` DayPhone String `xml:"DAYPHONE,omitempty"` EvePhone String `xml:"EVEPHONE,omitempty"` Email String `xml:"EMAIL,omitempty"` HolderType holderType `xml:"HOLDERTYPE,omitempty"` // One of INDIVIDUAL, JOINT, CUSTODIAL, TRUST, OTHER } // BankAcctInfo contains information about a bank account, including how to // access it (BankAcct), and whether it supports downloading transactions // (SupTxDl). type BankAcctInfo struct { XMLName xml.Name `xml:"BANKACCTINFO"` BankAcctFrom BankAcct `xml:"BANKACCTFROM"` SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only) XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer MaturityDate Date `xml:"MATURITYDATE,omitempty"` // Maturity date for CD, if CD MaturityAmt Amount `xml:"MATURITYAMOUNT,omitempty"` // Maturity amount for CD, if CD MinBalReq Amount `xml:"MINBALREQ,omitempty"` // Minimum balance required to avoid service fees AcctClassification acctClassification `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER OverdraftLimit Amount `xml:"OVERDRAFTLIMIT,omitempty"` SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE } // String makes pointers to BankAcctInfo structs print nicely func (bai *BankAcctInfo) String() string { return fmt.Sprintf("%+v", *bai) } // CCAcctInfo contains information about a credit card account, including how // to access it (CCAcct), and whether it supports downloading transactions // (SupTxDl). type CCAcctInfo struct { XMLName xml.Name `xml:"CCACCTINFO"` CCAcctFrom CCAcct `xml:"CCACCTFROM"` SupTxDl Boolean `xml:"SUPTXDL"` // Supports downloading transactions (as opposed to balance only) XferSrc Boolean `xml:"XFERSRC"` // Enabled as source for intra/interbank transfer XferDest Boolean `xml:"XFERDEST"` // Enabled as destination for intra/interbank transfer AcctClassification acctClassification `xml:"ACCTCLASSIFICATION,omitempty"` // One of PERSONAL, BUSINESS, CORPORATE, OTHER SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE } // String makes pointers to CCAcctInfo structs print nicely func (ci *CCAcctInfo) String() string { return fmt.Sprintf("%+v", *ci) } // InvAcctInfo contains information about an investment account, including how // to access it (InvAcct), and whether it supports downloading transactions // (SupTxDl). type InvAcctInfo struct { XMLName xml.Name `xml:"INVACCTINFO"` InvAcctFrom InvAcct `xml:"INVACCTFROM"` UsProductType usProductType `xml:"USPRODUCTTYPE"` // One of 401K, 403B, IRA, KEOGH, OTHER, SARSEP, SIMPLE, NORMAL, TDA, TRUST, UGMA Checking Boolean `xml:"CHECKING"` // Has check-writing privileges SvcStatus svcStatus `xml:"SVCSTATUS"` // One of AVAIL (available, but not yet requested), PEND (requested, but not yet available), ACTIVE InvAcctType holderType `xml:"INVACCTTYPE,omitempty"` // One of INDIVIDUAL, JOINT, TRUST, CORPORATE OptionLevel String `xml:"OPTIONLEVEL,omitempty"` // Text desribing option trading privileges } // String makes pointers to InvAcctInfo structs print nicely func (iai *InvAcctInfo) String() string { return fmt.Sprintf("%+v", *iai) } // AcctInfo represents generic account information. It should contain one (and // only one) *AcctInfo element corresponding to the tyep of account it // represents. type AcctInfo struct { XMLName xml.Name `xml:"ACCTINFO"` Name String `xml:"NAME,omitempty"` Desc String `xml:"DESC,omitempty"` Phone String `xml:"PHONE,omitempty"` PrimaryHolder HolderInfo `xml:"HOLDERINFO>PRIMARYHOLDER,omitempty"` SecondaryHolder HolderInfo `xml:"HOLDERINFO>SECONDARYHOLDER,omitempty"` // Only one of the rest of the fields will be valid for any given AcctInfo BankAcctInfo *BankAcctInfo `xml:"BANKACCTINFO,omitempty"` CCAcctInfo *CCAcctInfo `xml:"CCACCTINFO,omitempty"` InvAcctInfo *InvAcctInfo `xml:"INVACCTINFO,omitempty"` // TODO LOANACCTINFO // TODO BPACCTINFO? } // AcctInfoResponse contains the information about all a user's accounts // accessible from this FI type AcctInfoResponse struct { XMLName xml.Name `xml:"ACCTINFOTRNRS"` TrnUID UID `xml:"TRNUID"` Status Status `xml:"STATUS"` CltCookie String `xml:"CLTCOOKIE,omitempty"` // TODO `xml:"OFXEXTENSION,omitempty"` DtAcctUp Date `xml:"ACCTINFORS>DTACCTUP"` AcctInfo []AcctInfo `xml:"ACCTINFORS>ACCTINFO,omitempty"` } // Name returns the name of the top-level transaction XML/SGML element func (air *AcctInfoResponse) Name() string { return "ACCTINFOTRNRS" } // Valid returns (true, nil) if this struct was valid OFX when unmarshalled func (air *AcctInfoResponse) Valid(version ofxVersion) (bool, error) { if ok, err := air.TrnUID.Valid(); !ok { return false, err } //TODO implement return true, nil } // Type returns which message set this message belongs to (which Response // element of type []Message it belongs to) func (air *AcctInfoResponse) Type() messageType { return SignupRs } ================================================ FILE: signup_test.go ================================================ package ofxgo import ( "strings" "testing" "time" ) func TestMarshalAcctInfoRequest(t *testing.T) { var expectedString string = ` 20160115112300.000[-5:EST] myusername Pa$$word ENG BNK 1987 OFXGO 0001 e3ad9bda-38fa-4e5b-8099-1bd567ddef7a 20151221182945.000[-5:EST] ` EST := time.FixedZone("EST", -5*60*60) var client = BasicClient{ AppID: "OFXGO", AppVer: "0001", SpecVersion: OfxVersion203, } var request Request request.Signon.UserID = "myusername" request.Signon.UserPass = "Pa$$word" request.Signon.Org = "BNK" request.Signon.Fid = "1987" acctInfoRequest := AcctInfoRequest{ TrnUID: "e3ad9bda-38fa-4e5b-8099-1bd567ddef7a", DtAcctUp: *NewDate(2015, 12, 21, 18, 29, 45, 0, EST), } request.Signup = append(request.Signup, &acctInfoRequest) request.SetClientFields(&client) // Overwrite the DtClient value set by SetClientFields to time.Now() request.Signon.DtClient = *NewDate(2016, 1, 15, 11, 23, 0, 0, EST) marshalCheckRequest(t, &request, expectedString) } func TestUnmarshalAcctInfoResponse(t *testing.T) { responseReader := strings.NewReader(` 0 INFO 20060115112303 ENG 20050221091300 20060102160000 BNK 1987 10938754 0 INFO 20050228 Personal Checking 888-222-5827 8367556009 000999847 MONEYMRKT Y Y Y ACTIVE `) var expected Response expected.Version = OfxVersion203 expected.Signon.Status.Code = 0 expected.Signon.Status.Severity = "INFO" expected.Signon.DtServer = *NewDateGMT(2006, 1, 15, 11, 23, 03, 0) expected.Signon.Language = "ENG" expected.Signon.DtProfUp = NewDateGMT(2005, 2, 21, 9, 13, 0, 0) expected.Signon.DtAcctUp = NewDateGMT(2006, 1, 2, 16, 0, 0, 0) expected.Signon.Org = "BNK" expected.Signon.Fid = "1987" bankacctinfo := BankAcctInfo{ BankAcctFrom: BankAcct{ BankID: "8367556009", AcctID: "000999847", AcctType: AcctTypeMoneyMrkt, }, SupTxDl: true, XferSrc: true, XferDest: true, SvcStatus: SvcStatusActive, } acctInfoResponse := AcctInfoResponse{ TrnUID: "10938754", Status: Status{ Code: 0, Severity: "INFO", }, DtAcctUp: *NewDateGMT(2005, 2, 28, 0, 0, 0, 0), AcctInfo: []AcctInfo{{ Desc: "Personal Checking", Phone: "888-222-5827", BankAcctInfo: &bankacctinfo, }}, } expected.Signup = append(expected.Signup, &acctInfoResponse) response, err := ParseResponse(responseReader) if err != nil { t.Fatalf("Unexpected error unmarshalling response: %s\n", err) } checkResponsesEqual(t, &expected, response) checkResponseRoundTrip(t, response) } ================================================ FILE: types.go ================================================ package ofxgo import ( "crypto/rand" "errors" "fmt" "github.com/aclindsa/xml" "golang.org/x/text/currency" "math/big" "regexp" "strconv" "strings" "time" ) // Int provides helper methods to unmarshal int64 values from SGML/XML type Int int64 // UnmarshalXML handles unmarshalling an Int from an SGML/XML string. Leading // and trailing whitespace is ignored. func (i *Int) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } value = strings.TrimSpace(value) i2, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } *i = Int(i2) return nil } // Equal returns true if the two Ints are equal in value func (i Int) Equal(o Int) bool { return i == o } // Amount represents non-integer values (or at least values for fields that may // not necessarily be integers) type Amount struct { big.Rat } // UnmarshalXML handles unmarshalling an Amount from an SGML/XML string. // Leading and trailing whitespace is ignored. func (a *Amount) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } value = strings.TrimSpace(value) // The OFX spec allows the start of the fractional amount to be delineated // by a comma, so fix that up before attempting to parse it into big.Rat value = strings.Replace(value, ",", ".", 1) if _, ok := a.SetString(value); !ok { return errors.New("Failed to parse OFX amount") } return nil } // String prints a string representation of an Amount func (a Amount) String() string { return strings.TrimRight(strings.TrimRight(a.FloatString(100), "0"), ".") } // MarshalXML marshals an Amount to SGML/XML func (a Amount) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(a.String(), start) } // Equal returns true if two Amounts are equal in value func (a Amount) Equal(o Amount) bool { return (&a).Cmp(&o.Rat) == 0 } // Date represents OFX date/time values type Date struct { time.Time } var ofxDateFormats = []string{ "20060102150405.000", "20060102150405", "200601021504", "2006010215", "20060102", } var ofxDateZoneRegex = regexp.MustCompile(`^([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?$`) // UnmarshalXML handles unmarshalling a Date from an SGML/XML string. It // attempts to unmarshal the valid date formats in order of decreasing length // and defaults to GMT if a time zone is not provided, as per the OFX spec. // Leading and trailing whitespace is ignored. func (od *Date) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value, zone, zoneFormat string err := d.DecodeElement(&value, &start) if err != nil { return err } value = strings.SplitN(value, "]", 2)[0] value = strings.TrimSpace(value) // Split the time zone off, if any split := strings.SplitN(value, "[", 2) if len(split) == 2 { value = split[0] zoneFormat = " -0700" zone = strings.TrimRight(split[1], "]") matches := ofxDateZoneRegex.FindStringSubmatch(zone) if matches == nil { return errors.New("Invalid OFX Date timezone format: " + zone) } var err error var zonehours, zoneminutes int zonehours, err = strconv.Atoi(matches[1]) if err != nil { return err } if len(matches[3]) > 0 { zoneminutes, err = strconv.Atoi(matches[3]) if err != nil { return err } zoneminutes = zoneminutes * 60 / 100 } zone = fmt.Sprintf(" %+03d%02d", zonehours, zoneminutes) // Get the time zone name if it's there, default to GMT if the offset // is 0 and a name isn't supplied if len(matches[5]) > 0 { zone = zone + " " + matches[5] zoneFormat = zoneFormat + " MST" } else if zonehours == 0 && zoneminutes == 0 { zone = zone + " GMT" zoneFormat = zoneFormat + " MST" } } else { // Default to GMT if no time zone was specified zone = " +0000 GMT" zoneFormat = " -0700 MST" } // Try all the date formats, from longest to shortest for _, format := range ofxDateFormats { t, err := time.Parse(format+zoneFormat, value+zone) if err == nil { od.Time = t return nil } } return errors.New("OFX: Couldn't parse date:" + value) } // String returns a string representation of the Date abiding by the OFX spec func (od Date) String() string { format := od.Format(ofxDateFormats[0]) zonename, zoneoffset := od.Zone() if zoneoffset < 0 { format += "[" + fmt.Sprintf("%+d", zoneoffset/3600) } else { format += "[" + fmt.Sprintf("%d", zoneoffset/3600) } fractionaloffset := (zoneoffset % 3600) / 36 if fractionaloffset > 0 { format += "." + fmt.Sprintf("%02d", fractionaloffset) } else if fractionaloffset < 0 { format += "." + fmt.Sprintf("%02d", -fractionaloffset) } if len(zonename) > 0 { return format + ":" + zonename + "]" } return format + "]" } // MarshalXML marshals a Date to XML func (od Date) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(od.String(), start) } // Equal returns true if the two Dates represent the same time (time zones are // accounted for when comparing, but are not required to match) func (od Date) Equal(o Date) bool { return od.Time.Equal(o.Time) } // NewDate returns a new Date object with the provided date, time, and timezone func NewDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) *Date { return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} } var gmt = time.FixedZone("GMT", 0) // NewDateGMT returns a new Date object with the provided date and time in the // GMT timezone func NewDateGMT(year int, month time.Month, day, hour, min, sec, nsec int) *Date { return &Date{Time: time.Date(year, month, day, hour, min, sec, nsec, gmt)} } // String provides helper methods to unmarshal OFX string values from SGML/XML type String string // UnmarshalXML handles unmarshalling a String from an SGML/XML string. Leading // and trailing whitespace is ignored. func (os *String) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } *os = String(strings.TrimSpace(value)) return nil } // String returns the string func (os *String) String() string { return string(*os) } // Equal returns true if the two Strings are equal in value func (os String) Equal(o String) bool { return os == o } // Boolean provides helper methods to unmarshal bool values from OFX SGML/XML type Boolean bool // UnmarshalXML handles unmarshalling a Boolean from an SGML/XML string. // Leading and trailing whitespace is ignored. func (ob *Boolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } tmpob := strings.TrimSpace(value) switch tmpob { case "Y": *ob = Boolean(true) case "N": *ob = Boolean(false) default: return errors.New("Invalid OFX Boolean") } return nil } // MarshalXML marshals a Boolean to XML func (ob Boolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error { if ob { return e.EncodeElement("Y", start) } return e.EncodeElement("N", start) } // String returns a string representation of a Boolean value func (ob *Boolean) String() string { return fmt.Sprintf("%v", *ob) } // Equal returns true if the two Booleans are the same func (ob Boolean) Equal(o Boolean) bool { return ob == o } // UID represents an UID according to the OFX spec type UID string // UnmarshalXML handles unmarshalling an UID from an SGML/XML string. Leading // and trailing whitespace is ignored. func (ou *UID) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } *ou = UID(strings.TrimSpace(value)) return nil } // RecommendedFormat returns true iff this UID meets the OFX specification's // recommendation that UIDs follow the standard UUID 36-character format func (ou UID) RecommendedFormat() (bool, error) { if len(ou) != 36 { return false, errors.New("UID not 36 characters long") } if ou[8] != '-' || ou[13] != '-' || ou[18] != '-' || ou[23] != '-' { return false, errors.New("UID missing hyphens at the appropriate places") } return true, nil } // Valid returns true, nil if the UID is valid. This is less strict than // RecommendedFormat, and will always return true, nil if it does. func (ou UID) Valid() (bool, error) { if len(ou) == 0 || len(ou) > 36 { return false, errors.New("UID invalid length") } return true, nil } // Equal returns true if the two UIDs are the same func (ou UID) Equal(o UID) bool { return ou == o } // RandomUID creates a new randomly-generated UID func RandomUID() (*UID, error) { uidbytes := make([]byte, 16) n, err := rand.Read(uidbytes[:]) if err != nil { return nil, err } if n != 16 { return nil, errors.New("RandomUID failed to read 16 random bytes") } uid := UID(fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", uidbytes[:4], uidbytes[4:6], uidbytes[6:8], uidbytes[8:10], uidbytes[10:])) return &uid, nil } // CurrSymbol represents an ISO-4217 currency type CurrSymbol struct { currency.Unit } // UnmarshalXML handles unmarshalling a CurrSymbol from an SGML/XML string. // Leading and trailing whitespace is ignored. func (c *CurrSymbol) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var value string err := d.DecodeElement(&value, &start) if err != nil { return err } value = strings.TrimSpace(value) unit, err := currency.ParseISO(value) if err != nil { return errors.New("Error parsing CurrSymbol:" + err.Error()) } c.Unit = unit return nil } // MarshalXML marshals a CurrSymbol to SGML/XML func (c CurrSymbol) MarshalXML(e *xml.Encoder, start xml.StartElement) error { return e.EncodeElement(c.String(), start) } // Equal returns true if the two Currencies are the same func (c CurrSymbol) Equal(o CurrSymbol) bool { return c.String() == o.String() } // Valid returns true, nil if the CurrSymbol is valid. func (c CurrSymbol) Valid() (bool, error) { if c.String() == "XXX" { return false, fmt.Errorf("Invalid CurrSymbol: %s", c.Unit) } return true, nil } // NewCurrSymbol returns a new CurrSymbol given a three-letter ISO-4217 // currency symbol as a string func NewCurrSymbol(s string) (*CurrSymbol, error) { unit, err := currency.ParseISO(s) if err != nil { return nil, errors.New("Error parsing string to create new CurrSymbol:" + err.Error()) } return &CurrSymbol{unit}, nil } ================================================ FILE: types_test.go ================================================ package ofxgo import ( "fmt" "github.com/aclindsa/xml" "reflect" "testing" "time" ) func getTypeName(i interface{}) string { val := reflect.ValueOf(i) // Do the same thing that encoding/xml does to get the name for val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr { if val.IsNil() { return "" } val = val.Elem() } return val.Type().Name() } func marshalHelper(t *testing.T, expected string, i interface{}) { t.Helper() typename := getTypeName(i) expectedstring := fmt.Sprintf("<%s>%s", typename, expected, typename) b, err := xml.Marshal(i) if err != nil { t.Fatalf("Unexpected error on xml.Marshal(%T): %s\n", i, err) } if string(b) != expectedstring { t.Fatalf("Expected '%s', got '%s'\n", expectedstring, string(b)) } } func unmarshalHelper2(t *testing.T, input string, expected interface{}, overwritten interface{}, eq func(a, b interface{}) bool) { t.Helper() typename := getTypeName(expected) inputstring := fmt.Sprintf("<%s>%s", typename, input, typename) err := xml.Unmarshal([]byte(inputstring), &overwritten) if err != nil { t.Fatalf("Unexpected error on xml.Unmarshal(%T): %s\n", expected, err) } if !eq(overwritten, expected) { t.Fatalf("Expected '%s', got '%s'\n", expected, overwritten) } } func unmarshalHelper(t *testing.T, input string, expected interface{}, overwritten interface{}) { t.Helper() eq := func(a, b interface{}) bool { return reflect.DeepEqual(a, b) } unmarshalHelper2(t, input, expected, overwritten, eq) } func TestMarshalInt(t *testing.T) { var i Int = 927 marshalHelper(t, "927", &i) i = 0 marshalHelper(t, "0", &i) i = -768276587425 marshalHelper(t, "-768276587425", &i) } func TestUnmarshalInt(t *testing.T) { var i, overwritten Int = -48394, 0 unmarshalHelper(t, "-48394", &i, &overwritten) i = 0 unmarshalHelper(t, "0", &i, &overwritten) i = 198237198 unmarshalHelper(t, "198237198", &i, &overwritten) // Make sure stray newlines are handled properly unmarshalHelper(t, "198237198\n", &i, &overwritten) unmarshalHelper(t, "198237198\n\t", &i, &overwritten) } func TestMarshalAmount(t *testing.T) { var a Amount a.SetFrac64(8, 1) marshalHelper(t, "8", &a) a.SetFrac64(1, 8) marshalHelper(t, "0.125", &a) a.SetFrac64(-1, 200) marshalHelper(t, "-0.005", &a) a.SetInt64(0) marshalHelper(t, "0", &a) a.SetInt64(-768276587425) marshalHelper(t, "-768276587425", &a) a.SetFrac64(1, 12) marshalHelper(t, "0.0833333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", &a) type AmountStruct struct { A Amount } var as AmountStruct as.A.SetFrac64(1, 8) marshalHelper(t, "0.125", as) } func TestUnmarshalAmount(t *testing.T) { var a, overwritten Amount // Amount/big.Rat needs a special equality test because reflect.DeepEqual // doesn't always return equal for two values that big.Rat.Cmp() does eq := func(a, b interface{}) bool { if amountA, ok := a.(*Amount); ok { if amountB, ok2 := b.(*Amount); ok2 { return amountA.Cmp(&amountB.Rat) == 0 } } return false } a.SetFrac64(12, 1) unmarshalHelper2(t, "12", &a, &overwritten, eq) a.SetFrac64(-21309, 100) unmarshalHelper2(t, "-213.09", &a, &overwritten, eq) a.SetFrac64(8192, 1000) unmarshalHelper2(t, "8.192", &a, &overwritten, eq) unmarshalHelper2(t, "+8.192", &a, &overwritten, eq) a.SetInt64(0) unmarshalHelper2(t, "0", &a, &overwritten, eq) unmarshalHelper2(t, "+0", &a, &overwritten, eq) unmarshalHelper2(t, "-0", &a, &overwritten, eq) a.SetInt64(-19487135) unmarshalHelper2(t, "-19487135", &a, &overwritten, eq) // Make sure stray newlines are handled properly unmarshalHelper2(t, "-19487135\n", &a, &overwritten, eq) unmarshalHelper2(t, "-19487135\n \t ", &a, &overwritten, eq) } func TestAmountEqual(t *testing.T) { assertEq := func(a, b Amount) { if !a.Equal(b) { t.Fatalf("Amounts should be equal but Equal returned false: %s and %s\n", a, b) } } assertNEq := func(a, b Amount) { if a.Equal(b) { t.Fatalf("Amounts should not be equal but Equal returned true: %s and %s\n", a, b) } } var a, b Amount a.SetInt64(-19487135) b.SetInt64(-19487135) assertEq(a, b) b.SetInt64(19487135) assertNEq(a, b) b.SetInt64(0) assertNEq(a, b) a.SetInt64(-0) assertEq(a, b) a.SetFrac64(1, 1000000000000000000) b.SetFrac64(1, 1000000000000000001) assertNEq(a, b) } func TestMarshalDate(t *testing.T) { var d *Date UTC := time.FixedZone("UTC", 0) GMTNodesc := time.FixedZone("", 0) EST := time.FixedZone("EST", -5*60*60) NPT := time.FixedZone("NPT", (5*60+45)*60) IST := time.FixedZone("IST", (5*60+30)*60) NST := time.FixedZone("NST", -(3*60+30)*60) d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000) marshalHelper(t, "20170314150926.053[0:GMT]", d) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT) marshalHelper(t, "20170314150926.053[5.75:NPT]", d) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST) marshalHelper(t, "20170314150926.053[-5:EST]", d) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, UTC) marshalHelper(t, "20170314150926.053[0:UTC]", d) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST) marshalHelper(t, "20170314150926.053[5.50:IST]", d) d = NewDate(9999, 11, 1, 23, 59, 59, 1000, EST) marshalHelper(t, "99991101235959.000[-5:EST]", d) d = NewDate(0, 1, 1, 0, 0, 0, 0, IST) marshalHelper(t, "00000101000000.000[5.50:IST]", d) d = &Date{Time: time.Unix(0, 0).In(UTC)} marshalHelper(t, "19700101000000.000[0:UTC]", d) d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST) marshalHelper(t, "20170314000026.053[-5:EST]", d) d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST) marshalHelper(t, "20170314000026.053[-3.50:NST]", d) // Time zone without textual description d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMTNodesc) marshalHelper(t, "20170314150926.053[0]", d) type DateStruct struct { D Date } d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000) ds := DateStruct{D: *d} marshalHelper(t, "20170314150926.053[0:GMT]", ds) } func TestUnmarshalDate(t *testing.T) { var d *Date var overwritten Date GMT := time.FixedZone("GMT", 0) EST := time.FixedZone("EST", -5*60*60) NPT := time.FixedZone("NPT", (5*60+45)*60) IST := time.FixedZone("IST", (5*60+30)*60) NST := time.FixedZone("NST", -(3*60+30)*60) NSTNodesc := time.FixedZone("", -(3*60+30)*60) eq := func(a, b interface{}) bool { if dateA, ok := a.(*Date); ok { if dateB, ok2 := b.(*Date); ok2 { return dateA.Equal(*dateB) } } return false } // Ensure omitted fields default to the correct values d = NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000) unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq) unmarshalHelper2(t, "20170314150926.053", d, &overwritten, eq) d = NewDate(2017, 3, 14, 0, 0, 0, 0, GMT) unmarshalHelper2(t, "20170314", d, &overwritten, eq) // Ensure all signs on time zone offsets are properly handled 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]", d, &overwritten, eq) unmarshalHelper2(t, "20170314150926.053[+0]", d, &overwritten, eq) unmarshalHelper2(t, "20170314150926.053[-0]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, NPT) unmarshalHelper2(t, "20170314150926.053[5.75:NPT]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST) unmarshalHelper2(t, "20170314150926.053[-5:EST]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT) unmarshalHelper2(t, "20170314150926.053[0:GMT]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, IST) unmarshalHelper2(t, "20170314150926.053[5.50:IST]", d, &overwritten, eq) d = NewDate(2018, 11, 1, 23, 59, 58, 0, EST) unmarshalHelper2(t, "20181101235958.000[-5:EST]", d, &overwritten, eq) d = NewDate(0, 1, 1, 0, 0, 0, 0, IST) unmarshalHelper2(t, "00000101000000.000[5.50:IST]", d, &overwritten, eq) d = &Date{Time: time.Unix(0, 0).In(GMT)} unmarshalHelper2(t, "19700101000000.000[0:GMT]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, EST) unmarshalHelper2(t, "20170314000026.053[-5:EST]", d, &overwritten, eq) d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NST) unmarshalHelper2(t, "20170314000026.053[-3.50:NST]", d, &overwritten, eq) // Autopopulate zone without textual description for GMT d = NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT) unmarshalHelper2(t, "20170314150926.053[0]", d, &overwritten, eq) // but not for others: d = NewDate(2017, 3, 14, 0, 0, 26, 53*1000*1000, NSTNodesc) unmarshalHelper2(t, "20170314000026.053[-3.50]", d, &overwritten, eq) // Make sure we handle poorly-formatted dates (from Vanguard) 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 // Make sure we properly handle ending newlines 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\t", d, &overwritten, eq) } func TestDateEqual(t *testing.T) { GMT := time.FixedZone("GMT", 0) EST := time.FixedZone("EST", -5*60*60) assertEq := func(a, b *Date) { if !a.Equal(*b) { t.Fatalf("Dates should be equal but Equal returned false: %s and %s\n", *a, *b) } } assertNEq := func(a, b *Date) { if a.Equal(*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 gmt1 := NewDateGMT(2017, 3, 14, 15, 9, 26, 53*1000*1000) gmt2 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, GMT) est1 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000, EST) est2 := NewDate(2017, 3, 14, 10, 9, 26, 53*1000*1000+1, EST) est3 := NewDate(2017, 3, 14, 15, 9, 26, 53*1000*1000, EST) assertEq(gmt1, gmt2) assertEq(gmt2, gmt1) assertEq(gmt1, est1) assertNEq(gmt1, est2) assertNEq(est1, est2) assertNEq(gmt1, est3) } func TestMarshalString(t *testing.T) { var s String = "" marshalHelper(t, "", &s) s = "foo&bar" marshalHelper(t, "foo&bar", &s) s = "\n" marshalHelper(t, " ", &s) s = "Some Name" marshalHelper(t, "Some Name", &s) } func TestUnmarshalString(t *testing.T) { var s, overwritten String = "", "" unmarshalHelper(t, "", &s, &overwritten) s = "foo&bar" unmarshalHelper(t, "foo&bar", &s, &overwritten) // whitespace intentionally stripped because some OFX servers add newlines // inside tags s = "new\nline" unmarshalHelper(t, " new line ", &s, &overwritten) s = "Some Name" unmarshalHelper(t, "Some Name", &s, &overwritten) // Make sure stray newlines are handled properly 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) { var b Boolean = true marshalHelper(t, "Y", &b) b = false marshalHelper(t, "N", &b) type BooleanStruct struct { B Boolean } bs := BooleanStruct{B: true} marshalHelper(t, "Y", bs) } func TestUnmarshalBoolean(t *testing.T) { var b, overwritten Boolean = true, false unmarshalHelper(t, "Y", &b, &overwritten) b = false unmarshalHelper(t, "N", &b, &overwritten) // Make sure stray newlines are handled properly unmarshalHelper(t, "N\n", &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) { var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04" marshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u) } func TestUnmarshalUID(t *testing.T) { var u, overwritten UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04", "" unmarshalHelper(t, "d1cf3d3d-9ef9-4a97-b180-81706829cb04", &u, &overwritten) // Make sure stray newlines are handled properly u = "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab" unmarshalHelper(t, "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab\n", &u, &overwritten) unmarshalHelper(t, "0f94ce83-13b7-7568-e4fc-c02c7b47e7ab\n\t", &u, &overwritten) } func TestUIDRecommendedFormat(t *testing.T) { var u UID = "d1cf3d3d-9ef9-4a97-b180-81706829cb04" if ok, err := u.RecommendedFormat(); !ok || err != nil { t.Fatalf("UID unexpectedly failed validation\n") } u = "d1cf3d3d-9ef9-4a97-b180-81706829cb0" if ok, err := u.RecommendedFormat(); ok || err == nil { t.Fatalf("UID should have failed validation because it's too short\n") } u = "d1cf3d3d-9ef94a97-b180-81706829cb04" if ok, err := u.RecommendedFormat(); ok || err == nil { t.Fatalf("UID should have failed validation because it's missing hyphens\n") } u = "d1cf3d3d-9ef9-4a97-b180981706829cb04" if ok, err := u.RecommendedFormat(); ok || err == nil { t.Fatalf("UID should have failed validation because its hyphens have been replaced\n") } } func TestUIDValid(t *testing.T) { var u UID = "" if ok, err := u.Valid(); ok || err == nil { t.Fatalf("Empty UID unexpectedly valid\n") } u = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" if ok, err := u.Valid(); ok || err == nil { t.Fatalf("Too-long UID unexpectedly valid\n") } u = "7be37076-623a-425f-ae6b-a5465b7e93b0" if ok, err := u.Valid(); !ok || err != nil { t.Fatalf("Good UID unexpectedly invalid: %s\n", err.Error()) } } func TestRandomUID(t *testing.T) { uid, err := RandomUID() if err != nil { t.Fatalf("Unexpected error when calling RandomUID: %s\n", err) } if ok, err := uid.RecommendedFormat(); !ok || err != nil { t.Fatalf("UID generated with RandomUID() doesn't match recommended format: %s\n", err) } } func TestMarshalCurrSymbol(t *testing.T) { c, _ := NewCurrSymbol("USD") marshalHelper(t, "USD", &c) type CurrSymbolStruct struct { CS CurrSymbol } css := CurrSymbolStruct{CS: *c} marshalHelper(t, "USD", css) } func TestUnmarshalCurrSymbol(t *testing.T) { var overwritten CurrSymbol c, _ := NewCurrSymbol("USD") unmarshalHelper(t, "USD", c, &overwritten) // Make sure stray newlines are handled properly c, _ = NewCurrSymbol("EUR") unmarshalHelper(t, "EUR\n", c, &overwritten) unmarshalHelper(t, "EUR\n\t", c, &overwritten) } func TestCurrSymbolEqual(t *testing.T) { usd1, _ := NewCurrSymbol("USD") usd2, _ := NewCurrSymbol("USD") if !usd1.Equal(*usd2) { t.Fatalf("Two \"USD\" CurrSymbols returned !Equal()\n") } xxx, _ := NewCurrSymbol("XXX") if usd1.Equal(*xxx) { t.Fatalf("\"USD\" and \"XXX\" CurrSymbols returned Equal()\n") } } func TestCurrSymbolValid(t *testing.T) { var initial CurrSymbol ok, err := initial.Valid() if ok || err == nil { t.Fatalf("CurrSymbol unexpectedly returned Valid() for initial value\n") } ars, _ := NewCurrSymbol("ARS") ok, err = ars.Valid() if !ok || err != nil { t.Fatalf("CurrSymbol unexpectedly returned !Valid() for \"ARS\": %s\n", err.Error()) } xxx, _ := NewCurrSymbol("XXX") ok, err = xxx.Valid() if ok || err == nil { t.Fatalf("CurrSymbol unexpectedly returned Valid() for \"XXX\"\n") } } func TestNewCurrSymbol(t *testing.T) { curr, err := NewCurrSymbol("GBP") if err != nil { t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err) } if curr.String() != "GBP" { t.Fatalf("Created CurrSymbol doesn't print \"GBP\" as string representation\n") } curr, err = NewCurrSymbol("AFN") if err != nil { t.Fatalf("Unexpected error calling NewCurrSymbol: %s\n", err) } if curr.String() != "AFN" { t.Fatalf("Created CurrSymbol doesn't print \"AFN\" as string representation\n") } curr, err = NewCurrSymbol("BLAH") if err == nil { t.Fatalf("NewCurrSymbol didn't error on invalid currency identifier\n") } } ================================================ FILE: util.go ================================================ package ofxgo import ( "bytes" "github.com/aclindsa/xml" ) // Returns the next available Token from the xml.Decoder that is not CharData // made up entirely of whitespace. This is useful to skip whitespace when // manually unmarshaling XML. func nextNonWhitespaceToken(decoder *xml.Decoder) (xml.Token, error) { for { tok, err := decoder.Token() if err != nil { return nil, err } else if chars, ok := tok.(xml.CharData); ok { strippedBytes := bytes.TrimSpace(chars) if len(strippedBytes) != 0 { return tok, nil } } else { return tok, nil } } } ================================================ FILE: vanguard_client.go ================================================ package ofxgo import ( "crypto/tls" "errors" "io" "net/http" "strings" ) // VanguardClient provides a Client implementation which handles Vanguard's // cookie-passing requirements and also enables older, disabled-by-default // cipher suites. 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} } // vanguardHttpClient returns an http.Client with the default supported // ciphers plus the insecure ciphers Vanguard still uses. func vanguardHttpClient() *http.Client { var clientCiphers []uint16 vanguardCiphers := []uint16{ tls.TLS_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, } defaultCiphers := tls.CipherSuites() for _, cipher := range defaultCiphers { clientCiphers = append(clientCiphers, cipher.ID) } clientCiphers = append(clientCiphers, vanguardCiphers...) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ CipherSuites: clientCiphers, }, }, } return client } // RawRequest is a copy of BasicClient RawRequest with a custom http.Client // that enables older cipher suites. func (c *VanguardClient) 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") if c.UserAgent != "" { request.Header.Set("User-Agent", c.UserAgent) } client := vanguardHttpClient() response, err := client.Do(request) if err != nil { return nil, err } if response.StatusCode != 200 { return response, errors.New("OFXQuery request status: " + response.Status) } return response, nil } // rawRequestCookiesInsecureCiphers is RawRequest with the added features of // sending cookies and using a custom http.Client that enables older cipher // suites which are disabled by default func rawRequestCookiesInsecureCiphers(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) } client := vanguardHttpClient() response, err := client.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 rawRequestCookiesInsecureCiphers(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) }