diff --git a/.gitignore b/.gitignore
index e5ff729..500399c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
/exchange-rates
+/check-iban
+/check-vat
diff --git a/Makefile b/Makefile
index 17bb738..ee59de2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+run-check-iban:
+ go build -o check-iban github.com/pieterclaerhout/go-finance/cmd/check-iban
+ ./check-iban
+
run-check-vat:
go build -o check-vat github.com/pieterclaerhout/go-finance/cmd/check-vat
./check-vat
diff --git a/README.md b/README.md
index 2ab129d..c168c9b 100644
--- a/README.md
+++ b/README.md
@@ -65,3 +65,30 @@ func main() {
}
```
+
+## IBAN & BIC
+
+There is also a function which converts a regular Belgian Bank Account Number to it's IBAN / BIC equivalent:
+
+```go
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pieterclaerhout/go-finance"
+)
+
+func main() {
+
+ info, err := finance.CheckIBAN("738120256174")
+ if err != nil {
+ fmt.Println("ERROR:", err.Error())
+ os.Exit(1)
+ }
+
+ fmt.Println(info)
+
+}
+```
\ No newline at end of file
diff --git a/check-vat b/check-vat
deleted file mode 100755
index c629e54..0000000
Binary files a/check-vat and /dev/null differ
diff --git a/cmd/check-iban/main.go b/cmd/check-iban/main.go
new file mode 100644
index 0000000..da8eb5c
--- /dev/null
+++ b/cmd/check-iban/main.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/pieterclaerhout/go-finance"
+)
+
+func main() {
+
+ info, err := finance.CheckIBAN("738120256174")
+ if err != nil {
+ fmt.Println("ERROR:", err.Error())
+ os.Exit(1)
+ }
+
+ fmt.Println(info)
+
+}
diff --git a/iban_bic.go b/iban_bic.go
new file mode 100644
index 0000000..c64284f
--- /dev/null
+++ b/iban_bic.go
@@ -0,0 +1,111 @@
+package finance
+
+import (
+ "encoding/xml"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// DefaultIBANBICServiceURL is the default IBANBIC service URL to use
+const DefaultIBANBICServiceURL = "https://www.ibanbic.be/IBANBIC.asmx"
+
+// IBANBICServiceURL is the SOAP URL to be used when checking a bank account number
+var IBANBICServiceURL = DefaultIBANBICServiceURL
+
+// DefaultIBANBICTimeout is the default timeout to use when checking the bank account number
+const DefaultIBANBICTimeout = 5 * time.Second
+
+// IBANBICTimeout is the timeout to use when checking the bank account number
+var IBANBICTimeout = DefaultIBANBICTimeout
+
+var (
+ // ErrIBANBICServiceUnreachable is the error returned when the IBANBIC service is unreachable
+ ErrIBANBICServiceUnreachable = errors.New("IBANBIC service is unreachable")
+
+ // ErrIBANBICInvalidInput is the error returned when the bank account number is invalid
+ ErrIBANBICInvalidInput = errors.New("Number is not a valid bank account number")
+
+ // ErrIBANBICServiceError is the error returned when we get a non-standard error from the IBANBIC service
+ ErrIBANBICServiceError = "IBANBIC service returns an error: "
+)
+
+// IBANBICInfo contains the info about a Belgian Bank Account number
+type IBANBICInfo struct {
+ BBAN string // The Belgian Bank Account Number
+ BankName string // The name of the bank which issues the account
+ IBAN string // The IBAN number of the bank account
+ BIC string // The Bank Identification Code of the bank
+}
+
+// CheckIBAN checks the Bank Account Number and returns the IBAN and BIC information
+func CheckIBAN(number string) (*IBANBICInfo, error) {
+
+ if len(number) == 0 {
+ return nil, ErrIBANBICInvalidInput
+ }
+
+ result := &IBANBICInfo{
+ BBAN: number,
+ }
+
+ bankName, err := performIBANBICRequest("BBANtoBANKNAME", number)
+ if err != nil {
+ return nil, err
+ }
+ result.BankName = bankName
+
+ ibanAndBic, err := performIBANBICRequest("BBANtoIBANandBIC", number)
+ if err != nil {
+ return nil, err
+ }
+
+ ibanBicParts := strings.SplitN(ibanAndBic, "#", 2)
+ if len(ibanBicParts) < 2 {
+ return nil, errors.New(ErrIBANBICServiceError + "Failed to get BIC and IBAN code")
+ }
+
+ result.IBAN = ibanBicParts[0]
+ result.BIC = ibanBicParts[1]
+
+ return result, nil
+
+}
+
+func performIBANBICRequest(action string, value string) (string, error) {
+
+ url := IBANBICServiceURL + "/" + url.PathEscape(action) + "?Value=" + url.QueryEscape(value)
+
+ client := http.Client{
+ Timeout: VATTimeout,
+ }
+
+ res, err := client.Get(url)
+ if err != nil {
+ return "", ErrIBANBICServiceUnreachable
+ }
+ defer res.Body.Close()
+
+ xmlRes, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return "", err
+ }
+
+ xmlString := string(xmlRes)
+ if strings.Contains(xmlString, "Exception") {
+ exceptionParts := strings.Split(xmlString, "\n")
+ return "", errors.New(ErrIBANBICServiceError + strings.TrimSpace(exceptionParts[0]))
+ }
+
+ var result string
+ if err := xml.Unmarshal(xmlRes, &result); err != nil {
+ return "", err
+ }
+
+ return result, nil
+
+}
diff --git a/iban_bic_test.go b/iban_bic_test.go
new file mode 100644
index 0000000..24c8071
--- /dev/null
+++ b/iban_bic_test.go
@@ -0,0 +1,166 @@
+package finance_test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/pieterclaerhout/go-finance"
+)
+
+func Test_CheckIBAN(t *testing.T) {
+
+ type test struct {
+ number string
+ expectedBankName string
+ expectedIBAN string
+ expectedBIC string
+ expectsError bool
+ }
+
+ var tests = []test{
+ {"738-1202561-74", "KBC Bank", "BE16 7381 2025 6174", "KRED BE BB", false},
+ {"738120256174", "KBC Bank", "BE16 7381 2025 6174", "KRED BE BB", false},
+ {"7381202561-74", "KBC Bank", "BE16 7381 2025 6174", "KRED BE BB", false},
+ {"738-AAAAAAA-74", "", "", "", true},
+ {"738-AAAAAAA-AA", "", "", "", true},
+ {"", "", "", "", true},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.number, func(t *testing.T) {
+
+ info, err := finance.CheckIBAN(tc.number)
+
+ if tc.expectsError {
+
+ assert.Error(t, err, "error")
+ assert.Nil(t, info, "info")
+
+ } else {
+
+ assert.NoError(t, err, "error")
+ assert.NotNil(t, info, "info")
+
+ if info != nil {
+ assert.Equal(t, tc.expectedBankName, info.BankName, "bank-name")
+ assert.Equal(t, tc.expectedIBAN, info.IBAN, "IBAN")
+ assert.Equal(t, tc.expectedBIC, info.BIC, "BIC")
+ }
+
+ }
+
+ })
+ }
+
+}
+
+func Test_CheckIBAN_InvalidURL(t *testing.T) {
+
+ finance.IBANBICServiceURL = "ht&@-tp://:aa"
+ defer func() {
+ finance.IBANBICServiceURL = finance.DefaultIBANBICServiceURL
+ }()
+
+ result, err := finance.CheckIBAN("738120256174")
+
+ assert.Nil(t, result, "result")
+ assert.Error(t, err, "error")
+
+}
+
+func Test_CheckIBAN_Timeout(t *testing.T) {
+
+ s := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ time.Sleep(500 * time.Millisecond)
+ w.Header().Set("Content-Type", "text/xml")
+ w.Write([]byte("hello"))
+ }),
+ )
+ defer s.Close()
+
+ finance.IBANBICTimeout = 250 * time.Millisecond
+ finance.IBANBICServiceURL = s.URL
+ defer func() {
+ finance.IBANBICTimeout = finance.DefaultIBANBICTimeout
+ finance.IBANBICServiceURL = finance.DefaultIBANBICServiceURL
+ }()
+
+ result, err := finance.CheckIBAN("738120256174")
+
+ assert.Nil(t, result, "result")
+ assert.Error(t, err, "error")
+
+}
+
+func Test_CheckIBAN_ReadBodyError(t *testing.T) {
+
+ s := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte(`KBC Bank`))
+ }),
+ )
+ defer s.Close()
+
+ finance.IBANBICServiceURL = s.URL
+ defer func() {
+ finance.IBANBICServiceURL = finance.DefaultIBANBICServiceURL
+ }()
+
+ result, err := finance.CheckIBAN("738120256174")
+
+ assert.Nil(t, result, "result")
+ assert.Error(t, err, "error")
+
+}
+
+func Test_CheckIBAN_InvalidBody(t *testing.T) {
+
+ s := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Length", "1")
+ }),
+ )
+ defer s.Close()
+
+ finance.IBANBICServiceURL = s.URL
+ defer func() {
+ finance.IBANBICServiceURL = finance.DefaultIBANBICServiceURL
+ }()
+
+ result, err := finance.CheckIBAN("738120256174")
+
+ assert.Nil(t, result, "result")
+ assert.Error(t, err, "error")
+
+}
+
+func Test_CheckIBAN_PartialFail(t *testing.T) {
+
+ s := httptest.NewServer(
+ http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if strings.Contains(r.RequestURI, "BBANtoIBANandBIC") {
+ w.Header().Set("Content-Length", "1")
+ return
+ }
+ w.Write([]byte(`KBC Bank`))
+ }),
+ )
+ defer s.Close()
+
+ finance.IBANBICServiceURL = s.URL
+ defer func() {
+ finance.IBANBICServiceURL = finance.DefaultIBANBICServiceURL
+ }()
+
+ result, err := finance.CheckIBAN("738120256174")
+
+ assert.Nil(t, result, "result")
+ assert.Error(t, err, "error")
+
+}
diff --git a/vies.go b/vies.go
index b86f32a..6e177d8 100644
--- a/vies.go
+++ b/vies.go
@@ -3,7 +3,6 @@ package finance
import (
"bytes"
"encoding/xml"
- "fmt"
"io/ioutil"
"net/http"
"strings"
@@ -27,18 +26,6 @@ const DefaultVATServiceURL = "http://ec.europa.eu/taxation_customs/vies/services
// VATServiceURL is the SOAP URL to be used when checking a VAT number
var VATServiceURL = DefaultVATServiceURL
-const envelope = `
-
-
-
-
- {{countryCode}}
- {{vatNumber}}
-
-
-
-`
-
// DefaultVATTimeout is the default timeout to use when checking the VAT service
const DefaultVATTimeout = 5 * time.Second
@@ -64,7 +51,7 @@ func CheckVAT(vatNumber string) (*VATInfo, error) {
vatNumber = sanitizeVatNumber(vatNumber)
- e, err := buildEnvelope(vatNumber)
+ e, err := buildViewEnvelope(vatNumber)
if err != nil {
return nil, err
}
@@ -112,8 +99,6 @@ func CheckVAT(vatNumber string) (*VATInfo, error) {
return nil, err
}
- fmt.Sprintln(rd)
-
if rd.Soap.SoapFault.Message != "" {
return nil, errors.New(ErrVATserviceError + rd.Soap.SoapFault.Message)
}
@@ -141,17 +126,23 @@ func sanitizeVatNumber(vatNumber string) string {
return vatNumber
}
-// buildEnvelope parses envelope template
-func buildEnvelope(vatNumber string) (string, error) {
+// buildViewEnvelope parses envelope template
+func buildViewEnvelope(vatNumber string) (string, error) {
if len(vatNumber) < 3 {
return "", ErrVATNumberTooShort
}
- result := strings.TrimSpace(envelope)
- result = strings.ReplaceAll(result, "{{countryCode}}", strings.ToUpper(vatNumber[0:2]))
- result = strings.ReplaceAll(result, "{{vatNumber}}", strings.ToUpper(vatNumber[2:]))
+ envelope := `
+
+
+
+ ` + strings.ToUpper(vatNumber[0:2]) + `
+ ` + strings.ToUpper(vatNumber[2:]) + `
+
+
+`
- return result, nil
+ return envelope, nil
}
diff --git a/vies_test.go b/vies_test.go
index 75f948b..5d0af27 100644
--- a/vies_test.go
+++ b/vies_test.go
@@ -63,7 +63,7 @@ func Test_Check(t *testing.T) {
}
-func Test_Check_InvalidURL(t *testing.T) {
+func Test_CheckVAT_InvalidURL(t *testing.T) {
finance.VATServiceURL = "ht&@-tp://:aa"
defer func() {
@@ -77,7 +77,7 @@ func Test_Check_InvalidURL(t *testing.T) {
}
-func Test_Check_Timeout(t *testing.T) {
+func Test_CheckVAT_Timeout(t *testing.T) {
s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -102,7 +102,7 @@ func Test_Check_Timeout(t *testing.T) {
}
-func Test_Check_ReadBodyError(t *testing.T) {
+func Test_CheckVAT_ReadBodyError(t *testing.T) {
s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -123,7 +123,7 @@ func Test_Check_ReadBodyError(t *testing.T) {
}
-func Test_Check_InvalidInput(t *testing.T) {
+func Test_CheckVAT_InvalidInput(t *testing.T) {
s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -145,7 +145,7 @@ func Test_Check_InvalidInput(t *testing.T) {
}
-func Test_Check_InvalidXML(t *testing.T) {
+func Test_CheckVAT_InvalidXML(t *testing.T) {
s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -166,7 +166,7 @@ func Test_Check_InvalidXML(t *testing.T) {
}
-func Test_Check_SoapFault(t *testing.T) {
+func Test_CheckVAT_SoapFault(t *testing.T) {
s := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {