-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from jacob-ian/feat/ofx-ba
Add OFX support
- Loading branch information
Showing
11 changed files
with
246 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// The shared types for beanport | ||
package beanport | ||
|
||
// An import provider | ||
type Provider = string | ||
|
||
// The interface for a data importer | ||
type Importer interface { | ||
Import() ([]*PendingTransaction, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package ofx | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/aclindsa/ofxgo" | ||
) | ||
|
||
// Gets a description from an OFX transaction | ||
func description(txn ofxgo.Transaction) (string, error) { | ||
if txn.Payee != nil { | ||
return fmt.Sprintf("%s %s", txn.Payee.Name.String(), txn.Payee.State), nil | ||
} | ||
if txn.Name.String() != "" { | ||
return txn.Name.String(), nil | ||
} | ||
if txn.Memo.String() != "" { | ||
return descriptionFromMemo(txn.Memo.String()), nil | ||
} | ||
return "", fmt.Errorf("No available description properties for FITID %v", txn.FiTID.String()) | ||
} | ||
|
||
// Convert a bank transaction memo to a description | ||
func descriptionFromMemo(memo string) string { | ||
description := memo | ||
if description[:5] == "VISA-" { | ||
description = description[5:] | ||
} | ||
|
||
fields := strings.Fields(description) | ||
if len(fields) == 0 { | ||
return description | ||
} | ||
|
||
var output []string | ||
for i, f := range fields { | ||
if strings.Contains(f, "Ref") { | ||
continue | ||
} | ||
|
||
if i > 0 && fields[i-1] == "Apple" && f == "Pay" { | ||
continue | ||
} | ||
|
||
if i < len(fields)-1 && f == "Apple" && fields[i+1] == "Pay" { | ||
continue | ||
} | ||
|
||
output = append(output, f) | ||
} | ||
|
||
return strings.Join(output, " ") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package ofx | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/aclindsa/ofxgo" | ||
"github.com/jacob-ian/beanport/internal/beanport" | ||
) | ||
|
||
const ( | ||
Provider beanport.Provider = "ofx" | ||
) | ||
|
||
type ImporterConfig struct { | ||
Commodity string | ||
Account string | ||
} | ||
|
||
type Importer struct { | ||
data []byte | ||
commodity string | ||
account string | ||
} | ||
|
||
func (imp *Importer) Import() ([]*beanport.PendingTransaction, error) { | ||
res, err := ofxgo.ParseResponse(bytes.NewReader(imp.data)) | ||
if err != nil { | ||
return nil, errors.Join(errors.New("Couldn't parse OFX"), err) | ||
} | ||
|
||
if len(res.Bank) != 1 { | ||
return nil, errors.New("OFX error: too many bank messages") | ||
} | ||
|
||
root, ok := res.Bank[0].(*ofxgo.StatementResponse) | ||
if !ok { | ||
return nil, errors.Join(errors.New("OFX error: not a statement response"), err) | ||
} | ||
|
||
txns := root.BankTranList.Transactions | ||
var output []*beanport.PendingTransaction | ||
var parseErr error | ||
|
||
for i, txn := range txns { | ||
description, err := description(txn) | ||
if err != nil { | ||
parseErr = errors.Join(fmt.Errorf("Could not generate description"), err) | ||
break | ||
} | ||
|
||
amt, err := strconv.ParseFloat(txn.TrnAmt.String(), 64) | ||
if err != nil { | ||
parseErr = errors.Join(fmt.Errorf("Could not parse transaction amount for FITID %v: '%s'", txn.FiTID.String(), description), err) | ||
break | ||
} | ||
|
||
if amt == 0.00 { | ||
continue | ||
} | ||
|
||
ref := reference(txn, description, amt) | ||
|
||
output = append(output, &beanport.PendingTransaction{ | ||
Index: i, | ||
Account: imp.account, | ||
Date: txn.DtPosted.Time, | ||
Description: description, | ||
Amount: amt, | ||
Reference: ref, | ||
Commodity: imp.commodity, | ||
}) | ||
} | ||
|
||
if parseErr != nil { | ||
return nil, errors.Join(errors.New("Could not read transactions"), err) | ||
} | ||
|
||
return output, nil | ||
} | ||
|
||
func NewImporter(data []byte, config *ImporterConfig) *Importer { | ||
return &Importer{ | ||
data: data, | ||
commodity: config.Commodity, | ||
account: config.Account, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package ofx | ||
|
||
import ( | ||
"crypto/md5" | ||
"encoding/base64" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/aclindsa/ofxgo" | ||
) | ||
|
||
// Gets a reference for an OFX transaction | ||
// The reference is designed to prevent ledger duplicates | ||
func reference(txn ofxgo.Transaction, description string, amount float64) string { | ||
if txn.CheckNum.String() != "" { | ||
return txn.CheckNum.String() | ||
} | ||
if txn.RefNum.String() != "" { | ||
return txn.RefNum.String() | ||
} | ||
|
||
if ref := referenceFromMemo(txn.Memo.String()); ref != "" { | ||
return ref | ||
} | ||
|
||
input := fmt.Sprintf("%v:%s:%.2f", txn.DtPosted.Time.Format("2006-01-02"), description, amount) | ||
hashed := fmt.Sprintf("%x", md5.Sum([]byte(input))) | ||
encoded := base64.StdEncoding.EncodeToString([]byte(hashed)) | ||
return encoded | ||
} | ||
|
||
// Attempts to find a ref in the transaction memo | ||
func referenceFromMemo(memo string) string { | ||
if memo == "" { | ||
return "" | ||
} | ||
|
||
fields := strings.Fields(memo) | ||
if len(fields) == 0 { | ||
return "" | ||
} | ||
|
||
reference := "" | ||
for _, f := range fields { | ||
exists := strings.Contains(f, "Ref") | ||
if !exists { | ||
continue | ||
} | ||
reference = f | ||
break | ||
} | ||
|
||
return reference | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters