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"
2017-12-02 06:14:47 -05:00
"github.com/aclindsa/moneygo/internal/models"
2017-12-09 05:56:45 -05:00
"github.com/aclindsa/moneygo/internal/store"
2017-06-04 16:01:42 -04:00
"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-12-09 05:56:45 -05:00
func ofxImportHelper ( tx store . Tx , r io . Reader , user * models . 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-12-07 20:47:55 -05:00
account , err := tx . GetAccount ( 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-12-03 06:38:22 -05:00
var securitymap = make ( map [ int64 ] models . 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?
2017-12-04 05:55:25 -05:00
var transactions [ ] models . 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-12-04 05:55:25 -05:00
split . Status = models . 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-12-04 05:55:25 -05:00
if split . ImportSplitType == models . TradingAccount {
2017-09-20 21:30:17 -04:00
// 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-12-04 05:55:25 -05:00
} else if split . ImportSplitType == models . SubAccount {
subaccount := & models . Account {
2017-09-21 21:16:23 -04:00
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-12-04 05:55:25 -05:00
imbalances , err := GetTransactionImbalances ( tx , & transaction )
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
2017-12-04 05:55:25 -05:00
split := new ( models . Split )
2016-02-12 05:53:03 -05:00
r := new ( big . Rat )
r . Neg ( & imbalance )
2017-12-07 20:08:43 -05:00
security , err := tx . GetSecurity ( 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
}
2017-12-12 19:40:38 -05:00
split . Amount . Rat = * r
if split . Amount . Precision ( ) > security . Precision {
log . Printf ( "Precision on created imbalance-correction split (%d) greater than the underlying security (%s) allows (%d)" , split . Amount . Precision ( ) , security , security . Precision )
return NewError ( 999 /*Internal Error*/ )
}
2016-02-12 05:53:03 -05:00
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-12-08 21:27:03 -05:00
exists , err := tx . SplitExists ( split )
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-12-08 21:27:03 -05:00
err := tx . InsertTransaction ( & 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-12-02 06:14:47 -05:00
func OFXImportHandler ( context * Context , r * http . Request , user * models . User , accountid int64 ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
var ofxdownload OFXDownload
2017-11-13 20:48:19 -05:00
if err := ReadJSON ( r , & ofxdownload ) ; err != nil {
2017-10-14 14:20:50 -04:00
return NewError ( 3 /*Invalid Request*/ )
2017-06-04 16:01:42 -04:00
}
2017-12-07 20:47:55 -05:00
account , err := context . Tx . GetAccount ( 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
}
2017-12-04 05:55:25 -05:00
if account . Type == models . Investment {
2017-06-04 16:01:42 -04:00
// 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-11-12 20:17:27 -05:00
return ofxImportHelper ( context . Tx , response . Body , user , accountid )
2017-06-04 16:01:42 -04:00
}
2017-12-02 06:14:47 -05:00
func OFXFileImportHandler ( context * Context , r * http . Request , user * models . 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-11-12 20:17:27 -05:00
return ofxImportHelper ( context . 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-12-02 06:14:47 -05:00
func AccountImportHandler ( context * Context , r * http . Request , user * models . User , accountid int64 ) ResponseWriterWriter {
2017-06-04 16:01:42 -04:00
2017-11-12 21:12:49 -05:00
importType := context . NextLevel ( )
switch importType {
2017-06-04 16:01:42 -04:00
case "ofx" :
2017-11-12 20:17:27 -05:00
return OFXImportHandler ( context , r , user , accountid )
2017-06-04 16:01:42 -04:00
case "ofxfile" :
2017-11-12 20:17:27 -05:00
return OFXFileImportHandler ( context , 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
}
}
2017-11-12 20:17:27 -05:00
func ImportHandler ( r * http . Request , context * Context ) ResponseWriterWriter {
2017-11-12 20:38:22 -05:00
route := context . NextLevel ( )
if route != "gnucash" {
2017-11-12 20:17:27 -05:00
return NewError ( 3 /*Invalid Request*/ )
}
return GnucashImportHandler ( r , context )
}