Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#36: sepa account balances #38

Merged
merged 8 commits into from
Mar 2, 2024
97 changes: 79 additions & 18 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/mitch000001/go-hbci/dialog"
"github.com/mitch000001/go-hbci/domain"
"github.com/mitch000001/go-hbci/element"
"github.com/mitch000001/go-hbci/internal"
"github.com/mitch000001/go-hbci/message"
"github.com/mitch000001/go-hbci/segment"
"github.com/mitch000001/go-hbci/swift"
Expand All @@ -16,18 +17,19 @@ import (

// Config defines the basic configuration needed for a Client to work.
type Config struct {
BankID string `json:"bank_id"`
AccountID string `json:"account_id"`
PIN string `json:"pin"`
URL string `json:"url"`
HBCIVersion int `json:"hbci_version"`
Transport transport.Transport
BankID string `json:"bank_id"`
AccountID string `json:"account_id"`
PIN string `json:"pin"`
URL string `json:"url"`
HBCIVersion int `json:"hbci_version"`
Transport transport.Transport
EnableDebugLogging bool `json:"enable_debug_logging"`
}

func (c Config) hbciVersion() (segment.HBCIVersion, error) {
version, ok := segment.SupportedHBCIVersions[c.HBCIVersion]
if !ok {
return version, fmt.Errorf("Unsupported HBCI version. Supported versions are %v", domain.SupportedHBCIVersions)
return version, fmt.Errorf("unsupported HBCI version. Supported versions are %v", domain.SupportedHBCIVersions)
}
return version, nil
}
Expand All @@ -39,6 +41,7 @@ func (c Config) hbciVersion() (segment.HBCIVersion, error) {
// If the provided Config does not provide a URL or a HBCI-Version it will be
// looked up in the bankinfo database.
func New(config Config) (*Client, error) {
internal.SetDebugMode(config.EnableDebugLogging)
bankID := domain.BankID{
CountryCode: 280,
ID: config.BankID,
Expand All @@ -62,7 +65,7 @@ func New(config Config) (*Client, error) {
} else {
version, ok := segment.SupportedHBCIVersions[bankInfo.HbciVersion()]
if !ok {
return nil, fmt.Errorf("Unsupported HBCI version. Supported versions are %v", domain.SupportedHBCIVersions)
return nil, fmt.Errorf("unsupported HBCI version. Supported versions are %v", domain.SupportedHBCIVersions)
}
hbciVersion = version
}
Expand Down Expand Up @@ -252,23 +255,81 @@ func (c *Client) AccountBalances(account domain.AccountConnection, allAccounts b
return nil, err
}
var balances []domain.AccountBalance
balanceResponses := decryptedMessage.FindMarshaledSegments("HISAL")
if balanceResponses != nil {
for _, marshaledSegment := range balanceResponses {
balanceSegment := &segment.AccountBalanceResponseSegment{}
err = balanceSegment.UnmarshalHBCI(marshaledSegment)
if err != nil {
return nil, fmt.Errorf("error while parsing account balance: %v", err)
}
balances = append(balances, balanceSegment.AccountBalance())
balanceResponses := decryptedMessage.FindSegments(segment.AccountBalanceResponseID)
for _, unmarshaledSegment := range balanceResponses {
seg, ok := unmarshaledSegment.(segment.AccountBalanceResponse)
if !ok {
return nil, fmt.Errorf("malformed segment found with ID %q", segment.AccountBalanceResponseID)
}
} else {
balances = append(balances, seg.AccountBalance())
}
if len(balanceResponses) == 0 {
return nil, fmt.Errorf("malformed response: expected HISAL segment")
}

return balances, nil
}

// AccountBalances retrieves the balance for the provided account.
// If allAccounts is true it will fetch also the balances for all accounts
// associated with the account.
func (c *Client) SepaAccountBalances(account domain.InternationalAccountConnection, allAccounts bool, continuationReference string) ([]domain.SepaAccountBalance, error) {
if err := c.init(); err != nil {
return nil, err
}
builder := segment.NewBuilder(c.pinTanDialog.SupportedSegments())
accountBalanceRequest, err := builder.SepaAccountBalanceRequest(account, allAccounts)
if err != nil {
return nil, err
}
if continuationReference != "" {
accountBalanceRequest.SetContinuationMark(continuationReference)
}
decryptedMessage, err := c.pinTanDialog.SendMessage(
message.NewHBCIMessage(
c.hbciVersion,
c.hbciVersion.TanProcess4Request(segment.IdentificationID),
accountBalanceRequest,
),
)
if err != nil {
return nil, err
}
var balances []domain.SepaAccountBalance
balanceResponses := decryptedMessage.FindSegments(segment.AccountBalanceResponseID)
for _, unmarshaledSegment := range balanceResponses {
seg, ok := unmarshaledSegment.(segment.AccountBalanceResponse)
if !ok {
return nil, fmt.Errorf("malformed segment found with ID %q", segment.AccountBalanceResponseID)
}
sepaBalances, err := seg.SepaAccountBalance()
if err != nil {
return nil, fmt.Errorf("could not get sepa balances: %w", err)
}
balances = append(balances, sepaBalances)
}
if len(balanceResponses) == 0 {
return nil, fmt.Errorf("malformed response: expected HISAL segment")
}
var newContinuationReference string
acknowledgements := decryptedMessage.Acknowledgements()
for _, ack := range acknowledgements {
if ack.Code == element.AcknowledgementAdditionalInformation {
newContinuationReference = ack.Params[0]
break
}
}
if newContinuationReference == "" {
return balances, nil
}
nextBal, err := c.SepaAccountBalances(account, allAccounts, newContinuationReference)
if err != nil {
return nil, err
}
balances = append(balances, nextBal...)
return balances, nil
}

// Status returns information about open jobs to fetch from the institute.
// If a continuationReference is present, the status information attached to it
// will be fetched.
Expand Down
101 changes: 101 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,107 @@ func TestClientBalances(t *testing.T) {
}
}

func TestClientSepaBalances(t *testing.T) {
transport := &https.MockHTTPTransport{}
defer setMockHTTPTransport(transport)()

c := newTestClient()

account := domain.AccountInformation{
AccountConnection: domain.AccountConnection{AccountID: "100000000", CountryCode: 280, BankID: "10000000"},
UserID: "100000000",
Currency: "EUR",
Name1: "Muster",
Name2: "Max",
AllowedBusinessTransactions: []domain.BusinessTransaction{
{ID: "HKSAL", NeededSignatures: 1},
},
}

c.pinTanDialog.Accounts = []domain.AccountInformation{
account,
}

syncResponse := encryptedTestMessage(
"abcde",
"HIRMG:2:2:1+0020::Auftrag entgegengenommen'",
"HISYN:193:4:5+LRZYhZNbV2IBAAAd0?+VNqlkXrAQA'",
"HIBPA:2:2:+12+280:10000000+Bank Name+3+1+201:210:220+0'",
"HISALS:3:7:4+3+1'",
)
initResponse := encryptedTestMessage(
"abcde",
"HIRMG:2:2:1+0020::Auftrag entgegengenommen'",
"HIKIM:3:2+ec-Karte+Ihre neue ec-Karte liegt zur Abholung bereit.'",
)
balanceResponse := encryptedTestMessage(
"abcde",
"HIRMG:2:2:1+0020::Auftrag entgegengenommen'",
"HISAL:3:7:1+DE88100000000100000000:ABCDEFG1HIJ:100000000::280:10000000+Sichteinlagen+EUR+C:1000,15:EUR:20150812+C:20,:EUR:20150812+500,:EUR+1499,85:EUR'",
)
dialogEndResponseMessage := encryptedTestMessage("abcde", "HIRMG:2:2:1+0020::Der Auftrag wurde ausgeführt'")

transport.SetResponsePayloads([][]byte{
syncResponse,
dialogEndResponseMessage,
initResponse,
balanceResponse,
dialogEndResponseMessage,
})

accountConn := domain.InternationalAccountConnection{
IBAN: "DE88100000000100000000",
BIC: "ABCDEFG1HIJ",
AccountID: "100000000",
BankID: domain.BankID{
CountryCode: 280,
ID: "10000000",
},
}

balances, err := c.SepaAccountBalances(accountConn, true, "")
if err != nil {
t.Logf("Expected no error, got %T:%v\n", err, err)
t.Fail()
}

date, _ := time.Parse("20060102", "20150812")

expectedBalance := domain.SepaAccountBalance{
Account: domain.InternationalAccountConnection{
IBAN: "DE88100000000100000000",
BIC: "ABCDEFG1HIJ",
AccountID: "100000000",
BankID: domain.BankID{
CountryCode: 280,
ID: "10000000",
},
},
ProductName: "Sichteinlagen",
Currency: "EUR",
BookedBalance: domain.Balance{
Amount: domain.Amount{Amount: 1000.15, Currency: "EUR"},
TransmissionDate: date,
},
EarmarkedBalance: &domain.Balance{
Amount: domain.Amount{Amount: 20, Currency: "EUR"},
TransmissionDate: date,
},
CreditLimit: &domain.Amount{Amount: 500, Currency: "EUR"},
AvailableAmount: &domain.Amount{Amount: 1499.85, Currency: "EUR"},
}

if len(balances) != 1 {
t.Logf("Expected balances length to equal 1, was %d\n", len(balances))
t.Fail()
} else {
if !reflect.DeepEqual(balances[0], expectedBalance) {
t.Logf("Expected balance to equal\n%#v\n\tgot\n%#v\n", expectedBalance, balances[0])
t.Fail()
}
}
}

func newTestClient() *Client {
config := Config{
URL: "https://localhost",
Expand Down
21 changes: 17 additions & 4 deletions cmd/banking/balances.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"os"

"github.com/mitch000001/go-hbci/bankinfo"
"github.com/mitch000001/go-hbci/domain"
"github.com/mitch000001/go-hbci/iban"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -46,23 +47,35 @@ will fetch the balance for account 123456789.`,
}
account = domain.InternationalAccountConnection{
IBAN: string(i),
BIC: bankinfo.FindByBankID(clientConfig.BankID).BIC,
AccountID: balanceAccount,
BankID: domain.BankID{CountryCode: 280, ID: clientConfig.BankID},
}
balances, err := hbciClient.AccountBalances(account.ToAccountConnection(), true)
if disableSepa {
balances, err := hbciClient.AccountBalances(account.ToAccountConnection(), true)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println(domain.AccountBalances(balances).String())
return
}

balances, err := hbciClient.SepaAccountBalances(account, true, "")
if err != nil {
fmt.Println(err)
os.Exit(1)
}

fmt.Printf(domain.AccountBalances(balances).String())
fmt.Println(domain.SepaAccountBalances(balances).String())
},
}

func init() {
rootCmd.AddCommand(balancesCmd)

balancesCmd.Flags().BoolVar(
&disableSepa, "disableSepa", false,
"whether the library should not handle account data as sepa compliant",
)
balancesCmd.Flags().StringVar(
&balanceAccount, "accountID", "",
"the accountID to fetch balance for (defaults to the UserID)",
Expand Down
15 changes: 5 additions & 10 deletions cmd/banking/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (

"github.com/mitch000001/go-hbci/client"
"github.com/mitch000001/go-hbci/domain"
"github.com/mitch000001/go-hbci/internal"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -62,7 +61,6 @@ func init() {
cobra.OnInitialize(
initConfig,
initClient,
initLoggers,
)

// Here you will define your flags and configuration settings.
Expand Down Expand Up @@ -95,10 +93,11 @@ func initClient() {
os.Exit(1)
}
clientConfig = client.Config{
URL: url,
AccountID: userID,
BankID: blz,
PIN: PIN,
URL: url,
AccountID: userID,
BankID: blz,
PIN: PIN,
EnableDebugLogging: debug,
}
c, err := client.New(clientConfig)
if err != nil {
Expand Down Expand Up @@ -141,7 +140,3 @@ func initConfig() {
viper.WriteConfigAs(filepath.Join(home, ".banking.yaml"))
}
}

func initLoggers() {
internal.SetDebugMode(debug)
}
4 changes: 2 additions & 2 deletions cmd/unmarshaler/unmarshaler_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ func (s *segmentVersionsFlag) Set(in string) error {
for _, seg := range segments {
parts := strings.Split(seg, ":")
if len(parts) < 2 {
return fmt.Errorf("Malformed versioned segment: %q", seg)
return fmt.Errorf("malformed versioned segment: %q", seg)
}
version, err := strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("Malformed segment version: %v", err)
return fmt.Errorf("malformed segment version: %v", err)
}
var interfaceName string
if len(parts) == 3 {
Expand Down
4 changes: 2 additions & 2 deletions dialog/dialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"io"
"log"
"net"
"strconv"
Expand Down Expand Up @@ -587,7 +587,7 @@ func (d *dialog) request(clientMessage message.ClientMessage) (message.BankMessa

request := &transport.Request{
URL: d.hbciURL,
Body: ioutil.NopCloser(reqBody),
Body: io.NopCloser(reqBody),
}

response, err := d.transport.Do(request)
Expand Down
4 changes: 2 additions & 2 deletions dialog/mock_https_transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"io"

"github.com/mitch000001/go-hbci/transport"
)
Expand Down Expand Up @@ -113,7 +113,7 @@ func (m *mockHTTPSTransport) checkAndAdaptBoundaries(req *transport.Request) {
if m.errors == nil {
m.errors = make([]error, m.callCount)
}
reqBytes, err := ioutil.ReadAll(req.Body)
reqBytes, err := io.ReadAll(req.Body)
if err != nil {
m.errors = append(m.errors, fmt.Errorf("Unexpected request: %+#v\nBody: %v", req, err))
} else {
Expand Down
Loading
Loading