mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-10-30 15:50:04 -04:00
Aaron Lindsay
2ff1f47432
Simplify naming to remove "Tx" now that all handlers only have access to transactions anyway, and always use "tx" as the name of the variable representing the SQL transactions (to make it less likely to cause confusion with monetary transactions).
349 lines
10 KiB
Go
349 lines
10 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/aclindsa/ofxgo"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type OFXDownload struct {
|
|
OFXPassword string
|
|
StartDate time.Time
|
|
EndDate time.Time
|
|
}
|
|
|
|
func (od *OFXDownload) Read(json_str string) error {
|
|
dec := json.NewDecoder(strings.NewReader(json_str))
|
|
return dec.Decode(od)
|
|
}
|
|
|
|
func ofxImportHelper(tx *Tx, r io.Reader, user *User, accountid int64) ResponseWriterWriter {
|
|
itl, err := ImportOFX(r)
|
|
|
|
if err != nil {
|
|
//TODO is this necessarily an invalid request (what if it was an error on our end)?
|
|
log.Print(err)
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
if len(itl.Accounts) != 1 {
|
|
log.Printf("Found %d accounts when importing OFX, expected 1", len(itl.Accounts))
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
// Return Account with this Id
|
|
account, err := GetAccount(tx, accountid, user.UserId)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
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)
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
// 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
|
|
var securitymap = make(map[int64]Security)
|
|
for _, ofxsecurity := range itl.Securities {
|
|
// save off since ImportGetCreateSecurity overwrites SecurityId on
|
|
// ofxsecurity
|
|
oldsecurityid := ofxsecurity.SecurityId
|
|
security, err := ImportGetCreateSecurity(tx, user.UserId, &ofxsecurity)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
securitymap[oldsecurityid] = *security
|
|
}
|
|
|
|
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)
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
// TODO Ensure all transactions have at least one split in the account
|
|
// we're importing to?
|
|
|
|
var transactions []Transaction
|
|
for _, transaction := range itl.Transactions {
|
|
transaction.UserId = user.UserId
|
|
|
|
if !transaction.Valid() {
|
|
log.Print("Unexpected invalid transaction from OFX import")
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
|
|
// 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 {
|
|
split.Status = Imported
|
|
if split.AccountId != -1 {
|
|
if split.AccountId != importedAccount.AccountId {
|
|
log.Print("Imported split's AccountId wasn't -1 but also didn't match the account")
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
split.AccountId = account.AccountId
|
|
} else if split.SecurityId != -1 {
|
|
if sec, ok := securitymap[split.SecurityId]; ok {
|
|
// TODO try to auto-match splits to existing accounts based on past transactions that look like this one
|
|
if split.ImportSplitType == TradingAccount {
|
|
// Find/make trading account if we're that type of split
|
|
trading_account, err := GetTradingAccount(tx, user.UserId, sec.SecurityId)
|
|
if err != nil {
|
|
log.Print("Couldn't find split's SecurityId in map during OFX import")
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
split.AccountId = trading_account.AccountId
|
|
split.SecurityId = -1
|
|
} else if split.ImportSplitType == SubAccount {
|
|
subaccount := &Account{
|
|
UserId: user.UserId,
|
|
Name: sec.Name,
|
|
ParentAccountId: account.AccountId,
|
|
SecurityId: sec.SecurityId,
|
|
Type: account.Type,
|
|
}
|
|
subaccount, err := GetCreateAccount(tx, *subaccount)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
split.AccountId = subaccount.AccountId
|
|
split.SecurityId = -1
|
|
} else {
|
|
split.SecurityId = sec.SecurityId
|
|
}
|
|
} else {
|
|
log.Print("Couldn't find split's SecurityId in map during OFX import")
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
} else {
|
|
log.Print("Neither Split.AccountId Split.SecurityId was set during OFX import")
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
}
|
|
|
|
imbalances, err := transaction.GetImbalances(tx)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
|
|
// Fixup any imbalances in transactions
|
|
var zero big.Rat
|
|
for imbalanced_security, imbalance := range imbalances {
|
|
if imbalance.Cmp(&zero) != 0 {
|
|
imbalanced_account, err := GetImbalanceAccount(tx, user.UserId, imbalanced_security)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
|
|
// Add new split to fixup imbalance
|
|
split := new(Split)
|
|
r := new(big.Rat)
|
|
r.Neg(&imbalance)
|
|
security, err := GetSecurity(tx, imbalanced_security, user.UserId)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
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
|
|
// accounts. In the same loop, check to see if this transaction/split
|
|
// has been imported before
|
|
var already_imported bool
|
|
for _, split := range transaction.Splits {
|
|
if split.SecurityId != -1 || split.AccountId == -1 {
|
|
imbalanced_account, err := GetImbalanceAccount(tx, user.UserId, split.SecurityId)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
|
|
split.AccountId = imbalanced_account.AccountId
|
|
split.SecurityId = -1
|
|
}
|
|
|
|
exists, err := split.AlreadyImported(tx)
|
|
if err != nil {
|
|
log.Print("Error checking if split was already imported:", err)
|
|
return NewError(999 /*Internal Error*/)
|
|
} else if exists {
|
|
already_imported = true
|
|
}
|
|
}
|
|
|
|
if !already_imported {
|
|
transactions = append(transactions, transaction)
|
|
}
|
|
}
|
|
|
|
for _, transaction := range transactions {
|
|
err := InsertTransaction(tx, &transaction, user)
|
|
if err != nil {
|
|
log.Print(err)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
}
|
|
|
|
return SuccessWriter{}
|
|
}
|
|
|
|
func OFXImportHandler(tx *Tx, r *http.Request, user *User, accountid int64) ResponseWriterWriter {
|
|
download_json := r.PostFormValue("ofxdownload")
|
|
if download_json == "" {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
var ofxdownload OFXDownload
|
|
err := ofxdownload.Read(download_json)
|
|
if err != nil {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
account, err := GetAccount(tx, accountid, user.UserId)
|
|
if err != nil {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
ofxver := ofxgo.OfxVersion203
|
|
if len(account.OFXVersion) != 0 {
|
|
ofxver, err = ofxgo.NewOfxVersion(account.OFXVersion)
|
|
if err != nil {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
}
|
|
|
|
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)
|
|
return NewError(999 /*Internal Error*/)
|
|
}
|
|
|
|
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 {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
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...
|
|
log.Print(err)
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
return ofxImportHelper(tx, response.Body, user, accountid)
|
|
}
|
|
|
|
func OFXFileImportHandler(tx *Tx, r *http.Request, user *User, accountid int64) ResponseWriterWriter {
|
|
multipartReader, err := r.MultipartReader()
|
|
if err != nil {
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
|
|
// assume there is only one 'part'
|
|
part, err := multipartReader.NextPart()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return NewError(3 /*Invalid Request*/)
|
|
log.Print("Encountered unexpected EOF")
|
|
} else {
|
|
return NewError(999 /*Internal Error*/)
|
|
log.Print(err)
|
|
}
|
|
}
|
|
|
|
return ofxImportHelper(tx, part, user, accountid)
|
|
}
|
|
|
|
/*
|
|
* Assumes the User is a valid, signed-in user, but accountid has not yet been validated
|
|
*/
|
|
func AccountImportHandler(tx *Tx, r *http.Request, user *User, accountid int64, importtype string) ResponseWriterWriter {
|
|
|
|
switch importtype {
|
|
case "ofx":
|
|
return OFXImportHandler(tx, r, user, accountid)
|
|
case "ofxfile":
|
|
return OFXFileImportHandler(tx, r, user, accountid)
|
|
default:
|
|
return NewError(3 /*Invalid Request*/)
|
|
}
|
|
}
|