summary history files

internal/merge/merge.go
package merge

import (
	"bytes"
	"fmt"

	"github.com/aclindsa/ofxgo"
)

type Merger interface {
	Add([]byte) error
	Merge() ([]byte, error)
}

func NewOFXMerger() Merger {
	return &OFXMerger{}
}

type OFXMerger struct {
	ofxFiles ofxFiles
}

// ccMatch compares ofxgo.CCAcct c1 and c2 returning true if both accounts are
// the same and false if they are different.
func ccAccountsMatch(c1, c2 ofxgo.CCAcct) bool {
	if c1.AcctID != c2.AcctID {
		return false
	}
	if c1.AcctKey != c2.AcctKey {
		return false
	}

	return true

}

// bankAccountMatch compares ofxgo.BankAcct b1 and b2 returning true if both
// accounts are the same and false if they are different.
func bankAccountsMatch(b1, b2 ofxgo.BankAcct) bool {
	if b1.BankID != b2.BankID {
		return false
	}
	if b1.BranchID != b2.BranchID {
		return false
	}
	if b1.AcctID != b2.AcctID {
		return false
	}
	if b1.AcctType != b2.AcctType {
		return false
	}
	if b1.AcctKey != b2.AcctKey {
		return false
	}
	return true
}

// Add accepts a slice of bytes, parses bytes into a response and adds response
// to ofxFiles.
func (o *OFXMerger) Add(b []byte) error {
	var err error

	resp, err := ofxgo.ParseResponse(bytes.NewReader(b))
	if err != nil {
		return err
	}

	o.ofxFiles.statementType, err = getStatementType(resp)
	if err != nil {
		return err
	}

	ofxFile := ofxFile{resp: resp}

	if err = o.ofxFiles.validate(resp); err != nil {
		return err
	}

	switch o.ofxFiles.statementType {
	case statementTypeBank:
		stmt, ok := resp.Bank[0].(*ofxgo.StatementResponse)
		if !ok {
			return fmt.Errorf("failed to process bank statement")
		}
		o.ofxFiles.currSymbol = stmt.CurDef
		if o.ofxFiles.bankAccount != nil {
			if !bankAccountsMatch(*o.ofxFiles.bankAccount, stmt.BankAcctFrom) {
				return fmt.Errorf("bank accounts do not match")
			}
		} else {
			o.ofxFiles.bankAccount = &stmt.BankAcctFrom
		}
	case statementTypeCreditCard:
		stmt, ok := resp.CreditCard[0].(*ofxgo.CCStatementResponse)
		if !ok {
			return fmt.Errorf("failed to process credit card statement")
		}
		o.ofxFiles.currSymbol = stmt.CurDef

		if o.ofxFiles.ccAccount != nil {
			if !ccAccountsMatch(*o.ofxFiles.ccAccount, stmt.CCAcctFrom) {
				return fmt.Errorf("credit card accounts do not match")
			}
		} else {
			o.ofxFiles.ccAccount = &stmt.CCAcctFrom
		}
	}

	o.ofxFiles.files = append(o.ofxFiles.files, ofxFile)
	return nil
}

func newCCStatementResponse() (ofxgo.CCStatementResponse, error) {
	var err error
	var resp ofxgo.CCStatementResponse
	resp.Status = ofxgo.Status{
		Code:     ofxgo.Int(0),
		Severity: ofxgo.String("INFO"),
	}

	trnUID, err := ofxgo.RandomUID()
	if err != nil {
		return resp, err
	}
	resp.TrnUID = *trnUID

	return resp, nil
}

func newStatementResponse() (ofxgo.StatementResponse, error) {
	var err error
	var resp ofxgo.StatementResponse
	resp.Status = ofxgo.Status{
		Code:     ofxgo.Int(0),
		Severity: ofxgo.String("INFO"),
	}

	trnUID, err := ofxgo.RandomUID()
	if err != nil {
		return resp, err
	}
	resp.TrnUID = *trnUID

	return resp, nil
}

func (o *OFXMerger) Merge() ([]byte, error) {
	buf := new(bytes.Buffer)

	ts := NewTransactionSet()
	switch o.ofxFiles.statementType {
	case statementTypeCreditCard:
		stmt, err := newCCStatementResponse()
		if err != nil {
			return []byte{}, err
		}
		stmt.CurDef = o.ofxFiles.currSymbol
		stmt.DtAsOf = o.ofxFiles.dtAsOf()
		stmt.CCAcctFrom = *o.ofxFiles.ccAccount
		stmt.BankTranList = &ofxgo.TransactionList{
			Transactions: []ofxgo.Transaction{},
			DtEnd:        o.ofxFiles.dtEnd(),
			DtStart:      o.ofxFiles.dtStart(),
		}

		for idx, ofxFile := range o.ofxFiles.files {
			ofxFileStmt, ok := ofxFile.resp.CreditCard[0].(*ofxgo.CCStatementResponse)
			if !ok {
				return []byte{}, fmt.Errorf("unable to process credit card statement")
			}
			for _, i := range ofxFileStmt.BankTranList.Transactions {
				if ts.isDuplicate(idx, i) {
					continue
				}
				stmt.BankTranList.Transactions = append(stmt.BankTranList.Transactions, i)
			}
		}

		resp := ofxgo.Response{
			CreditCard: []ofxgo.Message{&stmt},
			Signon:     o.ofxFiles.signonResponse(),
		}
		buf, err = resp.Marshal()
		if err != nil {
			return []byte{}, err
		}
	case statementTypeBank:
		stmt, err := newStatementResponse()
		if err != nil {
			return []byte{}, err
		}

		stmt.CurDef = o.ofxFiles.currSymbol
		stmt.DtAsOf = o.ofxFiles.dtAsOf()
		stmt.BankAcctFrom = *o.ofxFiles.bankAccount
		stmt.BankTranList = &ofxgo.TransactionList{
			Transactions: []ofxgo.Transaction{},
			DtEnd:        o.ofxFiles.dtEnd(),
			DtStart:      o.ofxFiles.dtStart(),
		}
		for idx, ofxFile := range o.ofxFiles.files {
			ofxFileStmt, ok := ofxFile.resp.Bank[0].(*ofxgo.StatementResponse)
			if !ok {
				return []byte{}, fmt.Errorf("unable to process bank statement")
			}
			for _, i := range ofxFileStmt.BankTranList.Transactions {
				if ts.isDuplicate(idx, i) {
					continue
				}
				stmt.BankTranList.Transactions = append(stmt.BankTranList.Transactions, i)
			}
		}
		resp := ofxgo.Response{
			Bank:   []ofxgo.Message{&stmt},
			Signon: o.ofxFiles.signonResponse(),
		}

		buf, err = resp.Marshal()
		if err != nil {
			return []byte{}, err
		}
	default:
	}

	return buf.Bytes(), nil
}