1
0
mirror of https://github.com/aclindsa/moneygo.git synced 2024-09-21 12:20:04 -04:00
moneygo/internal/handlers/imports.go
Aaron Lindsay 2ff1f47432 Remove duplicate *Tx versions of database access methods
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).
2017-10-14 19:41:13 -04:00

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*/)
}
}