-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathbank.go
366 lines (338 loc) · 16 KB
/
bank.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
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 <DTSTART> 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
}