2017-10-04 19:35:59 -04:00
package handlers
2016-02-02 21:46:27 -05:00
import (
2017-06-04 16:01:42 -04:00
"encoding/json"
"github.com/aclindsa/ofxgo"
2016-02-02 21:46:27 -05:00
"io"
"log"
2016-02-12 05:53:03 -05:00
"math/big"
2016-02-02 21:46:27 -05:00
"net/http"
2017-06-04 16:01:42 -04:00
"strings"
"time"
2016-02-02 21:46:27 -05:00
)
2017-06-04 16:01:42 -04:00
type OFXDownload struct {
OFXPassword string
StartDate time . Time
EndDate time . Time
}
2016-02-02 21:46:27 -05:00
2017-06-04 16:01:42 -04:00
func ( od * OFXDownload ) Read ( json_str string ) error {
dec := json . NewDecoder ( strings . NewReader ( json_str ) )
return dec . Decode ( od )
}
2016-02-02 21:46:27 -05:00
2017-10-14 14:20:50 -04:00
func ofxImportHelper ( tx * Tx , r io . Reader , user * User , accountid int64 ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
itl , err := ImportOFX ( r )
2017-05-08 06:01:26 -04:00
2016-02-02 21:46:27 -05:00
if err != nil {
2017-05-08 06:01:26 -04:00
//TODO is this necessarily an invalid request (what if it was an error on our end)?
2016-02-02 21:46:27 -05:00
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2016-02-02 21:46:27 -05:00
}
2017-05-08 06:01:26 -04:00
if len ( itl . Accounts ) != 1 {
log . Printf ( "Found %d accounts when importing OFX, expected 1" , len ( itl . Accounts ) )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2016-02-02 21:46:27 -05:00
}
2017-05-08 06:01:26 -04:00
// Return Account with this Id
2017-10-14 19:41:13 -04:00
account , err := GetAccount ( tx , accountid , user . UserId )
2016-02-02 21:46:27 -05:00
if err != nil {
2017-05-08 06:01:26 -04:00
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2016-02-02 21:46:27 -05:00
}
2017-05-08 06:01:26 -04:00
importedAccount := itl . Accounts [ 0 ]
if len ( account . ExternalAccountId ) > 0 &&
account . ExternalAccountId != importedAccount . ExternalAccountId {
log . Printf ( "OFX import has \"%s\" as ExternalAccountId, but the account being imported to has\"%s\"" ,
importedAccount . ExternalAccountId ,
account . ExternalAccountId )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-05-08 06:01:26 -04:00
}
// Find matching existing securities or create new ones for those
// referenced by the OFX import. Also create a map from placeholder import
// SecurityIds to the actual SecurityIDs
2017-09-20 14:22:33 -04:00
var securitymap = make ( map [ int64 ] Security )
2017-05-08 06:01:26 -04:00
for _ , ofxsecurity := range itl . Securities {
2017-06-04 16:01:42 -04:00
// save off since ImportGetCreateSecurity overwrites SecurityId on
// ofxsecurity
oldsecurityid := ofxsecurity . SecurityId
2017-10-14 14:20:50 -04:00
security , err := ImportGetCreateSecurity ( tx , user . UserId , & ofxsecurity )
2017-05-08 06:01:26 -04:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-05-08 06:01:26 -04:00
}
2017-09-20 14:22:33 -04:00
securitymap [ oldsecurityid ] = * security
2017-05-08 06:01:26 -04:00
}
if account . SecurityId != securitymap [ importedAccount . SecurityId ] . SecurityId {
log . Printf ( "OFX import account's SecurityId (%d) does not match this account's (%d)" , securitymap [ importedAccount . SecurityId ] . SecurityId , account . SecurityId )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-05-08 06:01:26 -04:00
}
// TODO Ensure all transactions have at least one split in the account
// we're importing to?
2016-02-12 05:53:03 -05:00
var transactions [ ] Transaction
2017-05-08 06:01:26 -04:00
for _ , transaction := range itl . Transactions {
2016-02-12 05:53:03 -05:00
transaction . UserId = user . UserId
2016-02-02 21:46:27 -05:00
if ! transaction . Valid ( ) {
2017-05-08 06:01:26 -04:00
log . Print ( "Unexpected invalid transaction from OFX import" )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-02-02 21:46:27 -05:00
}
2017-05-08 06:01:26 -04:00
// Ensure that either AccountId or SecurityId is set for this split,
// and fixup the SecurityId to be a valid one for this user's actual
// securities instead of a placeholder from the import
for _ , split := range transaction . Splits {
2017-05-31 08:23:19 -04:00
split . Status = Imported
2017-05-08 06:01:26 -04:00
if split . AccountId != - 1 {
if split . AccountId != importedAccount . AccountId {
2017-09-20 20:13:01 -04:00
log . Print ( "Imported split's AccountId wasn't -1 but also didn't match the account" )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-05-08 06:01:26 -04:00
}
split . AccountId = account . AccountId
} else if split . SecurityId != - 1 {
if sec , ok := securitymap [ split . SecurityId ] ; ok {
2017-09-20 20:13:01 -04:00
// TODO try to auto-match splits to existing accounts based on past transactions that look like this one
2017-09-20 21:30:17 -04:00
if split . ImportSplitType == TradingAccount {
// Find/make trading account if we're that type of split
2017-10-14 14:20:50 -04:00
trading_account , err := GetTradingAccount ( tx , user . UserId , sec . SecurityId )
2017-09-20 21:30:17 -04:00
if err != nil {
log . Print ( "Couldn't find split's SecurityId in map during OFX import" )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-09-20 21:30:17 -04:00
}
split . AccountId = trading_account . AccountId
split . SecurityId = - 1
2017-09-21 21:16:23 -04:00
} else if split . ImportSplitType == SubAccount {
subaccount := & Account {
UserId : user . UserId ,
Name : sec . Name ,
ParentAccountId : account . AccountId ,
SecurityId : sec . SecurityId ,
Type : account . Type ,
}
2017-10-14 19:41:13 -04:00
subaccount , err := GetCreateAccount ( tx , * subaccount )
2017-09-21 21:16:23 -04:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-09-21 21:16:23 -04:00
}
split . AccountId = subaccount . AccountId
split . SecurityId = - 1
2017-09-20 21:30:17 -04:00
} else {
split . SecurityId = sec . SecurityId
}
2017-05-08 06:01:26 -04:00
} else {
log . Print ( "Couldn't find split's SecurityId in map during OFX import" )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-05-08 06:01:26 -04:00
}
} else {
log . Print ( "Neither Split.AccountId Split.SecurityId was set during OFX import" )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-05-08 06:01:26 -04:00
}
}
2017-10-14 19:41:13 -04:00
imbalances , err := transaction . GetImbalances ( tx )
2016-02-12 05:53:03 -05:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-02-12 05:53:03 -05:00
}
// Fixup any imbalances in transactions
var zero big . Rat
for imbalanced_security , imbalance := range imbalances {
if imbalance . Cmp ( & zero ) != 0 {
2017-10-14 14:20:50 -04:00
imbalanced_account , err := GetImbalanceAccount ( tx , user . UserId , imbalanced_security )
2016-02-12 05:53:03 -05:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-02-12 05:53:03 -05:00
}
// Add new split to fixup imbalance
split := new ( Split )
r := new ( big . Rat )
r . Neg ( & imbalance )
2017-10-14 19:41:13 -04:00
security , err := GetSecurity ( tx , imbalanced_security , user . UserId )
2016-10-16 08:19:11 -04:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-10-16 08:19:11 -04:00
}
2016-02-12 05:53:03 -05:00
split . Amount = r . FloatString ( security . Precision )
split . SecurityId = - 1
split . AccountId = imbalanced_account . AccountId
transaction . Splits = append ( transaction . Splits , split )
}
}
// Move any splits with SecurityId but not AccountId to Imbalances
2017-06-10 15:22:13 -04:00
// accounts. In the same loop, check to see if this transaction/split
// has been imported before
var already_imported bool
2016-02-12 05:53:03 -05:00
for _ , split := range transaction . Splits {
if split . SecurityId != - 1 || split . AccountId == - 1 {
2017-10-14 14:20:50 -04:00
imbalanced_account , err := GetImbalanceAccount ( tx , user . UserId , split . SecurityId )
2016-02-12 05:53:03 -05:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-02-12 05:53:03 -05:00
}
split . AccountId = imbalanced_account . AccountId
split . SecurityId = - 1
}
2017-06-10 15:22:13 -04:00
2017-10-14 19:41:13 -04:00
exists , err := split . AlreadyImported ( tx )
2017-06-10 15:22:13 -04:00
if err != nil {
log . Print ( "Error checking if split was already imported:" , err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-06-10 15:22:13 -04:00
} else if exists {
already_imported = true
}
2016-02-12 05:53:03 -05:00
}
2017-06-10 15:22:13 -04:00
if ! already_imported {
transactions = append ( transactions , transaction )
}
2016-02-02 21:46:27 -05:00
}
2016-02-12 05:53:03 -05:00
for _ , transaction := range transactions {
2017-10-14 19:41:13 -04:00
err := InsertTransaction ( tx , & transaction , user )
2016-02-02 21:46:27 -05:00
if err != nil {
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2016-02-02 21:46:27 -05:00
}
}
2017-10-14 14:20:50 -04:00
return SuccessWriter { }
2016-02-02 21:46:27 -05:00
}
2017-06-04 16:01:42 -04:00
2017-10-14 14:20:50 -04:00
func OFXImportHandler ( tx * Tx , r * http . Request , user * User , accountid int64 ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
download_json := r . PostFormValue ( "ofxdownload" )
if download_json == "" {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
var ofxdownload OFXDownload
err := ofxdownload . Read ( download_json )
if err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
2017-10-14 14:20:50 -04:00
account , err := GetAccount ( tx , accountid , user . UserId )
2017-06-04 16:01:42 -04:00
if err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
ofxver := ofxgo . OfxVersion203
if len ( account . OFXVersion ) != 0 {
ofxver , err = ofxgo . NewOfxVersion ( account . OFXVersion )
if err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
}
var client = ofxgo . Client {
AppID : account . OFXAppID ,
AppVer : account . OFXAppVer ,
SpecVersion : ofxver ,
NoIndent : account . OFXNoIndent ,
}
var query ofxgo . Request
query . URL = account . OFXURL
query . Signon . ClientUID = ofxgo . UID ( account . OFXClientUID )
query . Signon . UserID = ofxgo . String ( account . OFXUser )
query . Signon . UserPass = ofxgo . String ( ofxdownload . OFXPassword )
query . Signon . Org = ofxgo . String ( account . OFXORG )
query . Signon . Fid = ofxgo . String ( account . OFXFID )
transactionuid , err := ofxgo . RandomUID ( )
if err != nil {
log . Println ( "Error creating uid for transaction:" , err )
2017-10-14 14:20:50 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-06-04 16:01:42 -04:00
}
if account . Type == Investment {
// Investment account
statementRequest := ofxgo . InvStatementRequest {
TrnUID : * transactionuid ,
InvAcctFrom : ofxgo . InvAcct {
BrokerID : ofxgo . String ( account . OFXBankID ) ,
AcctID : ofxgo . String ( account . OFXAcctID ) ,
} ,
Include : true ,
IncludeOO : true ,
IncludePos : true ,
IncludeBalance : true ,
Include401K : true ,
Include401KBal : true ,
}
query . InvStmt = append ( query . InvStmt , & statementRequest )
} else if account . OFXAcctType == "CC" {
// Import credit card transactions
statementRequest := ofxgo . CCStatementRequest {
TrnUID : * transactionuid ,
CCAcctFrom : ofxgo . CCAcct {
AcctID : ofxgo . String ( account . OFXAcctID ) ,
} ,
Include : true ,
}
query . CreditCard = append ( query . CreditCard , & statementRequest )
} else {
// Import generic bank transactions
acctTypeEnum , err := ofxgo . NewAcctType ( account . OFXAcctType )
if err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
statementRequest := ofxgo . StatementRequest {
TrnUID : * transactionuid ,
BankAcctFrom : ofxgo . BankAcct {
BankID : ofxgo . String ( account . OFXBankID ) ,
AcctID : ofxgo . String ( account . OFXAcctID ) ,
AcctType : acctTypeEnum ,
} ,
Include : true ,
}
query . Bank = append ( query . Bank , & statementRequest )
}
response , err := client . RequestNoParse ( & query )
if err != nil {
// TODO this could be an error talking with the OFX server...
2017-09-20 20:13:01 -04:00
log . Print ( err )
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
defer response . Body . Close ( )
2017-10-14 14:20:50 -04:00
return ofxImportHelper ( tx , response . Body , user , accountid )
2017-06-04 16:01:42 -04:00
}
2017-10-14 14:20:50 -04:00
func OFXFileImportHandler ( tx * Tx , r * http . Request , user * User , accountid int64 ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
multipartReader , err := r . MultipartReader ( )
if err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
// assume there is only one 'part'
part , err := multipartReader . NextPart ( )
if err != nil {
if err == io . EOF {
2017-09-20 20:13:01 -04:00
log . Print ( "Encountered unexpected EOF" )
2017-10-14 20:38:40 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
} else {
log . Print ( err )
2017-10-14 20:38:40 -04:00
return NewError ( 999 /*Internal Error*/ )
2017-06-04 16:01:42 -04:00
}
}
2017-10-14 14:20:50 -04:00
return ofxImportHelper ( tx , part , user , accountid )
2017-06-04 16:01:42 -04:00
}
/ *
* Assumes the User is a valid , signed - in user , but accountid has not yet been validated
* /
2017-10-14 14:20:50 -04:00
func AccountImportHandler ( tx * Tx , r * http . Request , user * User , accountid int64 , importtype string ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
switch importtype {
case "ofx" :
2017-10-14 14:20:50 -04:00
return OFXImportHandler ( tx , r , user , accountid )
2017-06-04 16:01:42 -04:00
case "ofxfile" :
2017-10-14 14:20:50 -04:00
return OFXFileImportHandler ( tx , r , user , accountid )
2017-06-04 16:01:42 -04:00
default :
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
}