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) {