diff --git a/README.md b/README.md
index 4be3955..1bde679 100644
--- a/README.md
+++ b/README.md
@@ -2,11 +2,12 @@ Package vat
===
![Build](https://github.com/Teamwork/vat/actions/workflows/build.yml/badge.svg)
-[![Go Report Card](https://goreportcard.com/badge/github.com/teamwork/vat/v2)](https://goreportcard.com/report/github.com/teamwork/vat/v2)
-[![GoDoc](https://godoc.org/github.com/teamwork/vat/v2?status.svg)](https://godoc.org/github.com/teamwork/vat/v2)
+[![Go Report Card](https://goreportcard.com/badge/github.com/teamwork/vat/v3)](https://goreportcard.com/report/github.com/teamwork/vat/v3)
+[![GoDoc](https://godoc.org/github.com/teamwork/vat/v3?status.svg)](https://godoc.org/github.com/teamwork/vat/v3)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/teamwork/vat/master/LICENSE)
-Package for validating VAT numbers & retrieving VAT rates (from [ibericode/vat-rates](https://github.com/ibericode/vat-rates)) in Go.
+Package for validating VAT numbers & retrieving VAT rates (
+from [ibericode/vat-rates](https://github.com/ibericode/vat-rates)) in Go.
Based on https://github.com/dannyvankooten/vat
@@ -28,7 +29,13 @@ import "github.com/teamwork/vat"
### Validating VAT numbers
-VAT numbers can be validated by format, existence or both. VAT numbers are looked up using the [VIES VAT validation API](http://ec.europa.eu/taxation_customs/vies/).
+VAT numbers can be validated by format, existence or both.
+
+EU VAT numbers are looked up using the [VIES VAT validation API](http://ec.europa.eu/taxation_customs/vies/).
+
+UK VAT numbers are looked up
+using [UK GOV VAT validation API](https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/1.0)
+.
```go
package main
@@ -36,20 +43,24 @@ package main
import "github.com/teamwork/vat"
func main() {
- // Validate number by format + existence
- validity, err := vat.ValidateNumber("NL123456789B01")
+ // These validation functions return an error if the VAT number is invalid. If no error, then it is valid.
+
+ // Validate number by format + existence
+ err := vat.Validate("NL123456789B01")
- // Validate number format
- validity, err := vat.ValidateNumberFormat("NL123456789B01")
+ // Validate number format
+ err := vat.ValidateFormat("NL123456789B01")
- // Validate number existence
- validity, err := vat.ValidateNumberExistence("NL123456789B01")
+ // Validate number existence
+ err := vat.ValidateExists("NL123456789B01")
}
```
### Retrieving VAT rates
-> This package relies on a [community maintained repository of vat rates](https://github.com/ibericode/vat-rates). We invite you to toggle notifications for that repository and contribute changes to VAT rates in your country once they are announced.
+> This package relies on a [community maintained repository of vat rates](https://github.com/ibericode/vat-rates). We
+> invite you to toggle notifications for that repository and contribute changes to VAT rates in your country once they
+> are announced.
To get VAT rate periods for a country, first get a CountryRates struct using the country's ISO-3166-1-alpha2 code.
@@ -59,16 +70,16 @@ You can get the rate that is currently in effect using the `GetRate` function.
package main
import (
- "fmt"
- "github.com/teamwork/vat"
+ "fmt"
+ "github.com/teamwork/vat"
)
func main() {
- c, err := vat.GetCountryRates("IE")
- r, err := c.GetRate("standard")
+ c, err := vat.GetCountryRates("IE")
+ r, err := c.GetRate("standard")
- fmt.Printf("Standard VAT rate for IE is %.2f", r)
- // Output: Standard VAT rate for IE is 23.00
+ fmt.Printf("Standard VAT rate for IE is %.2f", r)
+ // Output: Standard VAT rate for IE is 23.00
}
```
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..0be741d
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,28 @@
+package vat
+
+import (
+ "errors"
+ "fmt"
+)
+
+// ErrInvalidVATNumberFormat will be returned if a VAT with an invalid format is given
+var ErrInvalidVATNumberFormat = errors.New("vat: VAT number format is invalid")
+
+// ErrVATNumberNotFound will be returned if the given VAT number is not found in the external lookup service
+var ErrVATNumberNotFound = errors.New("vat: number not found as an existing active VAT number")
+
+// ErrInvalidCountryCode indicates that this package could not find a country matching the VAT number prefix
+var ErrInvalidCountryCode = errors.New("vat: unknown country code")
+
+// ErrInvalidRateLevel will be returned when getting wrong rate level
+var ErrInvalidRateLevel = errors.New("vat: unknown rate level")
+
+// ErrServiceUnavailable will be returned when the service is unreachable
+type ErrServiceUnavailable struct {
+ Err error
+}
+
+// Error returns the error message
+func (e ErrServiceUnavailable) Error() string {
+ return fmt.Sprintf("vat: service is unreachable: %v", e.Err)
+}
diff --git a/gen.go b/gen.go
new file mode 100644
index 0000000..cb928cb
--- /dev/null
+++ b/gen.go
@@ -0,0 +1,3 @@
+//go:generate mockgen -destination=mocks/mock_lookup_service.go -package=mocks github.com/teamwork/vat/v3 LookupServiceInterface
+
+package vat
diff --git a/go.mod b/go.mod
index 125b6bb..be2ce61 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/teamwork/vat/v2
+module github.com/teamwork/vat/v3
go 1.21
diff --git a/lookup_service.go b/lookup_service.go
deleted file mode 100644
index c05f81a..0000000
--- a/lookup_service.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package vat
-
-import (
- "bytes"
- "net/http"
- "time"
-)
-
-// ViesServiceInterface is an interface for the service that calls VIES.
-type ViesServiceInterface interface {
- Lookup(envelope string) (*http.Response, error)
-}
-
-// ViesService implements ViesServiceInterface using the HTTP client.
-type ViesService struct{}
-
-// Lookup calls the VIES service to get info about the VAT number
-func (s *ViesService) Lookup(envelope string) (*http.Response, error) {
- envelopeBuffer := bytes.NewBufferString(envelope)
- client := http.Client{
- Timeout: time.Duration(ServiceTimeout) * time.Second,
- }
- return client.Post(serviceURL, "text/xml;charset=UTF-8", envelopeBuffer)
-}
-
-const serviceURL = "http://ec.europa.eu/taxation_customs/vies/services/checkVatService"
-
-// ServiceTimeout indicates the number of seconds before a service request times out.
-var ServiceTimeout = 10
diff --git a/mocks/mock_lookup_service.go b/mocks/mock_lookup_service.go
new file mode 100644
index 0000000..0f6ef65
--- /dev/null
+++ b/mocks/mock_lookup_service.go
@@ -0,0 +1,48 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/teamwork/vat/v3 (interfaces: LookupServiceInterface)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+ reflect "reflect"
+
+ gomock "github.com/golang/mock/gomock"
+)
+
+// MockLookupServiceInterface is a mock of LookupServiceInterface interface.
+type MockLookupServiceInterface struct {
+ ctrl *gomock.Controller
+ recorder *MockLookupServiceInterfaceMockRecorder
+}
+
+// MockLookupServiceInterfaceMockRecorder is the mock recorder for MockLookupServiceInterface.
+type MockLookupServiceInterfaceMockRecorder struct {
+ mock *MockLookupServiceInterface
+}
+
+// NewMockLookupServiceInterface creates a new mock instance.
+func NewMockLookupServiceInterface(ctrl *gomock.Controller) *MockLookupServiceInterface {
+ mock := &MockLookupServiceInterface{ctrl: ctrl}
+ mock.recorder = &MockLookupServiceInterfaceMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLookupServiceInterface) EXPECT() *MockLookupServiceInterfaceMockRecorder {
+ return m.recorder
+}
+
+// Validate mocks base method.
+func (m *MockLookupServiceInterface) Validate(arg0 string) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Validate", arg0)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Validate indicates an expected call of Validate.
+func (mr *MockLookupServiceInterfaceMockRecorder) Validate(arg0 interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockLookupServiceInterface)(nil).Validate), arg0)
+}
diff --git a/mocks/mock_vies_service.go b/mocks/mock_vies_service.go
deleted file mode 100644
index 0ab4fc7..0000000
--- a/mocks/mock_vies_service.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/teamwork/vat/v2 (interfaces: ViesServiceInterface)
-
-// Package mocks is a generated GoMock package.
-package mocks
-
-import (
- http "net/http"
- reflect "reflect"
-
- gomock "github.com/golang/mock/gomock"
-)
-
-// MockViesServiceInterface is a mock of ViesServiceInterface interface.
-type MockViesServiceInterface struct {
- ctrl *gomock.Controller
- recorder *MockViesServiceInterfaceMockRecorder
-}
-
-// MockViesServiceInterfaceMockRecorder is the mock recorder for MockViesServiceInterface.
-type MockViesServiceInterfaceMockRecorder struct {
- mock *MockViesServiceInterface
-}
-
-// NewMockViesServiceInterface creates a new mock instance.
-func NewMockViesServiceInterface(ctrl *gomock.Controller) *MockViesServiceInterface {
- mock := &MockViesServiceInterface{ctrl: ctrl}
- mock.recorder = &MockViesServiceInterfaceMockRecorder{mock}
- return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockViesServiceInterface) EXPECT() *MockViesServiceInterfaceMockRecorder {
- return m.recorder
-}
-
-// Lookup mocks base method.
-func (m *MockViesServiceInterface) Lookup(arg0 string) (*http.Response, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Lookup", arg0)
- ret0, _ := ret[0].(*http.Response)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Lookup indicates an expected call of Lookup.
-func (mr *MockViesServiceInterfaceMockRecorder) Lookup(arg0 interface{}) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lookup", reflect.TypeOf((*MockViesServiceInterface)(nil).Lookup), arg0)
-}
diff --git a/numbers.go b/numbers.go
deleted file mode 100644
index 5c1d8d2..0000000
--- a/numbers.go
+++ /dev/null
@@ -1,181 +0,0 @@
-package vat
-
-import (
- "bytes"
- "encoding/xml"
- "errors"
- "io"
- "regexp"
- "strings"
-)
-
-// ViesResponse holds the response data from the Vies call
-type ViesResponse struct {
- CountryCode string
- VATNumber string
- RequestDate string
- Valid bool
- Name string
- Address string
-}
-
-// ErrInvalidVATNumber will be returned when an invalid VAT number is passed to a function that validates existence.
-var ErrInvalidVATNumber = errors.New("vat: vat number is invalid")
-
-// ErrCountryNotFound indicates that this package could not find a country matching the VAT number prefix.
-var ErrCountryNotFound = errors.New("vat: country not found")
-
-// ErrServiceUnavailable will be returned when VIES VAT validation API or jsonvat.com is unreachable.
-var ErrServiceUnavailable = errors.New("vat: service is unreachable")
-
-// ValidateNumber validates a VAT number by both format and existence.
-// The existence check uses the VIES VAT validation SOAP API and will only run when format validation passes.
-func ValidateNumber(n string) (bool, error) {
- isValidFormat, err := ValidateNumberFormat(n)
- existence := false
-
- if isValidFormat {
- existence, err = ValidateNumberExistence(n)
- }
-
- return isValidFormat && existence, err
-}
-
-// ValidateNumberFormat validates a VAT number by its format.
-func ValidateNumberFormat(n string) (bool, error) {
- patterns := map[string]string{
- "AT": `U[A-Z0-9]{8}`,
- "BE": `(0[0-9]{9}|[0-9]{10})`,
- "BG": `[0-9]{9,10}`,
- "CH": `(?:E(?:-| )[0-9]{3}(?:\.| )[0-9]{3}(?:\.| )[0-9]{3}( MWST)?|E[0-9]{9}(?:MWST)?)`,
- "CY": `[0-9]{8}[A-Z]`,
- "CZ": `[0-9]{8,10}`,
- "DE": `[0-9]{9}`,
- "DK": `[0-9]{8}`,
- "EE": `[0-9]{9}`,
- "EL": `[0-9]{9}`,
- "ES": `[A-Z][0-9]{7}[A-Z]|[0-9]{8}[A-Z]|[A-Z][0-9]{8}`,
- "FI": `[0-9]{8}`,
- "FR": `([A-Z]{2}|[0-9]{2})[0-9]{9}`,
- "GB": `[0-9]{9}|[0-9]{12}|(GD|HA)[0-9]{3}`,
- "HR": `[0-9]{11}`,
- "HU": `[0-9]{8}`,
- "IE": `[A-Z0-9]{7}[A-Z]|[A-Z0-9]{7}[A-W][A-I]`,
- "IT": `[0-9]{11}`,
- "LT": `([0-9]{9}|[0-9]{12})`,
- "LU": `[0-9]{8}`,
- "LV": `[0-9]{11}`,
- "MT": `[0-9]{8}`,
- "NL": `[0-9]{9}B[0-9]{2}`,
- "PL": `[0-9]{10}`,
- "PT": `[0-9]{9}`,
- "RO": `[0-9]{2,10}`,
- "SE": `[0-9]{12}`,
- "SI": `[0-9]{8}`,
- "SK": `[0-9]{10}`,
- }
-
- if len(n) < 3 {
- return false, nil
- }
-
- n = strings.ToUpper(n)
- pattern, ok := patterns[n[0:2]]
- if !ok {
- return false, ErrCountryNotFound
- }
-
- matched, err := regexp.MatchString(pattern, n[2:])
- return matched, err
-}
-
-// ValidateNumberExistence validates a VAT number by its existence using the VIES VAT API (using SOAP)
-func ValidateNumberExistence(vatNumber string) (bool, error) {
- r, err := Lookup(vatNumber, &ViesService{})
- if err != nil {
- return false, err
- }
- return r.Valid, nil
-}
-
-// Lookup returns *ViesResponse for a VAT number
-func Lookup(vatNumber string, service ViesServiceInterface) (*ViesResponse, error) {
- if len(vatNumber) < 3 {
- return nil, ErrInvalidVATNumber
- }
-
- res, err := service.Lookup(getEnvelope(vatNumber))
- if err != nil {
- return nil, ErrServiceUnavailable
- }
- defer func() {
- _ = res.Body.Close()
- }()
-
- xmlRes, err := io.ReadAll(res.Body)
- if err != nil {
- return nil, err
- }
-
- // check if response contains "INVALID_INPUT" string
- if bytes.Contains(xmlRes, []byte("INVALID_INPUT")) {
- return nil, ErrInvalidVATNumber
- }
-
- // check if response contains "MS_UNAVAILABLE" string
- if bytes.Contains(xmlRes, []byte("MS_UNAVAILABLE")) ||
- bytes.Contains(xmlRes, []byte("MS_MAX_CONCURRENT_REQ")) {
- return nil, ErrServiceUnavailable
- }
-
- var rd struct {
- XMLName xml.Name `xml:"Envelope"`
- Soap struct {
- XMLName xml.Name `xml:"Body"`
- Soap struct {
- XMLName xml.Name `xml:"checkVatResponse"`
- CountryCode string `xml:"countryCode"`
- VATNumber string `xml:"vatNumber"`
- RequestDate string `xml:"requestDate"` // 2015-03-06+01:00
- Valid bool `xml:"valid"`
- Name string `xml:"name"`
- Address string `xml:"address"`
- }
- }
- }
- if err = xml.Unmarshal(xmlRes, &rd); err != nil {
- return nil, err
- }
-
- r := &ViesResponse{
- CountryCode: rd.Soap.Soap.CountryCode,
- VATNumber: rd.Soap.Soap.VATNumber,
- RequestDate: rd.Soap.Soap.RequestDate,
- Valid: rd.Soap.Soap.Valid,
- Name: rd.Soap.Soap.Name,
- Address: rd.Soap.Soap.Address,
- }
-
- return r, nil
-}
-
-// getEnvelope parses envelope template
-func getEnvelope(n string) string {
- n = strings.ToUpper(n)
- countryCode := n[0:2]
- vatNumber := n[2:]
- const envelopeTemplate = `
-
-
-
- {{.countryCode}}
- {{.vatNumber}}
-
-
-`
-
- e := envelopeTemplate
- e = strings.Replace(e, "{{.countryCode}}", countryCode, 1)
- e = strings.Replace(e, "{{.vatNumber}}", vatNumber, 1)
- return e
-}
diff --git a/numbers_test.go b/numbers_test.go
deleted file mode 100644
index 01d9ec4..0000000
--- a/numbers_test.go
+++ /dev/null
@@ -1,278 +0,0 @@
-package vat
-
-import (
- "bytes"
- "fmt"
- "io"
- "net/http"
- "strconv"
- "testing"
-
- "github.com/golang/mock/gomock"
- "github.com/teamwork/vat/v2/mocks"
-)
-
-var tests = []struct {
- number string
- valid bool
-}{
- {"", false},
- {"A", false},
- {"AB123A01", false},
- {"ATU12345678", true},
- {"ATU15673009", true},
- {"ATU1234567", false},
- {"BE0123456789", true},
- {"BE1234567891", true},
- {"BE0999999999", true},
- {"BE9999999999", true},
- {"BE012345678", false},
- {"BE123456789", false},
- {"BG123456789", true},
- {"BG1234567890", true},
- {"BG1234567", false},
- {"CHE-156.730.098 MWST", true},
- {"CHE-156.730.098", true},
- {"CHE156730098MWST", true},
- {"CHE156730098", true},
- {"CY12345678X", true},
- {"CY15673009L", true},
- {"CY1234567X", false},
- {"CZ12345678", true},
- {"CZ1234567", false},
- {"DE123456789", true},
- {"DE12345678", false},
- {"DK12345678", true},
- {"DK1234567", false},
- {"EE123456789", true},
- {"EE12345678", false},
- {"EL123456789", true},
- {"EL12345678", false},
- {"ESX12345678", true},
- {"ESX1234567", false},
- {"FI1234567", false},
- {"FI12345678", true},
- {"FR12345678901", true},
- {"FR1234567890", false},
- {"GB999999973", true},
- {"GB156730098481", true},
- {"GBGD549", true},
- {"GBHA549", true},
- {"GB99999997", false},
- {"HU12345678", true},
- {"HU1234567", false},
- {"HR12345678901", true},
- {"HR1234567890", false},
- {"IE1234567X", true},
- {"IE123456X", false},
- {"IT12345678901", true},
- {"IT1234567890", false},
- {"LT123456789", true},
- {"LT12345678", false},
- {"LU26375245", true},
- {"LU12345678", true},
- {"LU1234567", false},
- {"LV12345678901", true},
- {"LV1234567890", false},
- {"MT12345678", true},
- {"MT1234567", false},
- {"NL123456789B01", true},
- {"NL123456789B12", true},
- {"NL12345678B12", false},
- {"PL1234567890", true},
- {"PL123456789", false},
- {"PT123456789", true},
- {"PT12345678", false},
- {"RO123456789", true},
- {"RO1", false}, // Romania has a really weird VAT format...
- {"SE123456789012", true},
- {"SE12345678901", false},
- {"SI12345678", true},
- {"SI1234567", false},
- {"SK1234567890", true},
- {"SK123456789", false},
- {"KL123456789B12", false},
- {"NL12343678B12", false},
- {"PL1224567890", true},
- {"PL123456989", false},
- {"PT125456789", true},
- {"PT16345678", false},
- {"RO123456789", true},
- {"KT123456789", false},
- {"LT12335678", false},
- {"LU26275245", true},
- {"LU14345678", true},
- {"LU1234567", false},
- {"LQ12345678901", false},
- {"QE123456789", false},
- {"DE12375670", false},
- {"DK123365678", true},
- {"DO1231567", false},
- {"EE123456789", true},
- {"EL123434678", true},
- {"EL122456789", true},
- {"EL12245678", false},
-
- {"CY1134567X", false},
- {"CZ17345678", true},
- {"CZ1239567", false},
- {"DE123456789", true},
- {"DE12325678", false},
- {"DK12385678", true},
- {"DK1234767", false},
- {"EE123456689", true},
- {"EE12343678", false},
- {"EL123426789", true},
- {"EL12342678", false},
- {"ESX12315678", true},
- {"ESX1634567", false},
- {"FI1274567", false},
- {"FI12385678", true},
- {"FR12349678901", true},
- {"FR1234597890", false},
- {"GB999979973", true},
- {"GB15673098481", true},
- {"GBGD529", true},
- {"GBHA519", true},
- {"GB99997997", false},
- {"HU12342678", true},
- {"HU1234167", false},
- {"HR12341678901", true},
- {"HR1234267890", false},
- {"IE1234167X", true},
- {"IE123356X", false},
- {"IT12245678901", true},
- {"IT1231567890", false},
- {"LT123156789", true},
- {"LT12343678", false},
- {"LU26371245", true},
- {"LU12325678", true},
- {"LU1214567", false},
- {"LV12345578901", true},
- {"LV1234367890", false},
- {"MT12325678", true},
- {"MT1234167", false},
- {"NL123156789B01", true},
- {"NL113456789B12", true},
- {"NL12315678B12", false},
- {"PO1234567890", false},
-}
-
-func BenchmarkValidateFormat(b *testing.B) {
- for i := 0; i < b.N; i++ {
- _, _ = ValidateNumberFormat("NL" + strconv.Itoa(i))
- }
-}
-
-func TestValidateNumberFormat(t *testing.T) {
- for _, test := range tests {
- valid, err := ValidateNumberFormat(test.number)
- if err != nil {
- if err.Error() != ErrCountryNotFound.Error() {
- panic(err)
- }
- }
-
- if test.valid != valid {
- t.Errorf("Expected %v for %v, got %v", test.valid, test.number, valid)
- }
-
- }
-}
-
-var lookupTests = []struct {
- fullVatNumber string
- countryCode string
- vatNumber string
- isValid bool
- expectedResponse string
- expectedError *error
-}{
- {"BE0472429986", "BE", "0472429986", true, "", nil},
- {"NL123456789B01", "NL", "123456789B01", false, "", nil},
- {"Hi", "HI", "", false, "", &ErrInvalidVATNumber},
- {"INVALID INPUT", "IN", "VALID INPUT", false, "INVALID_INPUT", &ErrInvalidVATNumber},
- {"BE0472429986", "BE", "0472429986", true, "MS_UNAVAILABLE", &ErrServiceUnavailable},
- {"BE0472429986", "BE", "0472429986", true, "MS_MAX_CONCURRENT_REQ", &ErrServiceUnavailable},
-}
-
-func TestLookupValidateNumberExistence(t *testing.T) {
-
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
-
- mockViesService := mocks.NewMockViesServiceInterface(ctrl)
-
- for _, test := range lookupTests {
- if len(test.fullVatNumber) >= 3 {
- expectedViesResponseBody := test.expectedResponse
- if expectedViesResponseBody == "" {
- expectedViesResponseBody = expectedViesResponse(test.vatNumber, test.countryCode, test.isValid)
- }
- expectedViesResponse := &http.Response{
- Body: io.NopCloser(bytes.NewBufferString(expectedViesResponseBody)),
- StatusCode: http.StatusOK,
- }
-
- mockViesService.EXPECT().
- Lookup(expectedViesRequestEnvelope(test.vatNumber, test.countryCode)).
- Return(expectedViesResponse, nil)
- }
-
- viesResponse, err := Lookup(test.fullVatNumber, mockViesService)
- if err != nil {
- if test.expectedError == nil {
- t.Error(fmt.Errorf("unexpected error: %w", err))
- } else if err != *test.expectedError {
- t.Error(fmt.Errorf("Expected error: %w\nGot: %s", *test.expectedError, err.Error()))
- }
- } else {
- if viesResponse == nil {
- t.Error("expected a ViesResponse, none received")
- }
- if test.isValid && !viesResponse.Valid {
- t.Errorf("expected %s to be a valid VAT number.", test.fullVatNumber)
- }
- if !test.isValid && viesResponse.Valid {
- t.Errorf("expected %s to not be a valid VAT number.", test.fullVatNumber)
- }
- }
- }
-}
-
-func expectedViesRequestEnvelope(vatNumber string, countryCode string) string {
- return fmt.Sprintf(
- `
-
-
-
- %s
- %s
-
-
-`,
- countryCode,
- vatNumber,
- )
-}
-
-func expectedViesResponse(vatNumber string, countryCode string, isValid bool) string {
- return fmt.Sprintf(
- `
-
-
-
- %s
- %s
- 2024-01-24+01:00
- %s
- VAT HOLDER'S NAME'
- VAT HOLDER'S ADDRESS
-
-
- `,
- countryCode,
- vatNumber,
- strconv.FormatBool(isValid),
- )
-}
diff --git a/rates.go b/rates.go
index b4a47c7..a80689f 100644
--- a/rates.go
+++ b/rates.go
@@ -2,7 +2,6 @@ package vat
import (
"encoding/json"
- "errors"
"net/http"
"strings"
"sync"
@@ -24,12 +23,6 @@ type CountryRates struct {
var mutex = &sync.Mutex{} // protect countriesRates
var countriesRates []CountryRates
-// ErrInvalidCountryCode will be returned when calling GetCountryRates with an invalid country code
-var ErrInvalidCountryCode = errors.New("vat: unknown country code")
-
-// ErrInvalidRateLevel will be returned when getting wrong rate level
-var ErrInvalidRateLevel = errors.New("vat: unknown rate level")
-
// GetRateOn returns the effective VAT rate on a given date
func (cr *CountryRates) GetRateOn(t time.Time, level string) (float32, error) {
var activePeriod RatePeriod
@@ -90,7 +83,7 @@ func GetRates() ([]CountryRates, error) {
// FetchRates fetches the latest VAT rates from ibericode/vat-rates and updates the in-memory rates
func FetchRates() ([]CountryRates, error) {
client := http.Client{
- Timeout: (time.Duration(ServiceTimeout) * time.Second),
+ Timeout: time.Duration(ViesServiceTimeout) * time.Second,
}
r, err := client.Get("https://raw.githubusercontent.com/ibericode/vat-rates/master/vat-rates.json")
if err != nil {
@@ -111,7 +104,7 @@ func FetchRates() ([]CountryRates, error) {
return nil, err
}
- rates := []CountryRates{}
+ var rates []CountryRates
for code, periods := range apiResponse.Items {
rate := CountryRates{CountryCode: code}
for _, period := range periods {
diff --git a/uk_vat_service.go b/uk_vat_service.go
new file mode 100644
index 0000000..bd1eabf
--- /dev/null
+++ b/uk_vat_service.go
@@ -0,0 +1,48 @@
+package vat
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+)
+
+// ukVATService is service that calls a UK VAT API to validate UK VAT numbers.
+type ukVATService struct{}
+
+// Validate checks if the given VAT number exists and is active. If no error is returned, then it is.
+func (s *ukVATService) Validate(vatNumber string) error {
+ vatNumber = strings.ToUpper(vatNumber)
+
+ // Only VAT numbers starting with "GB" are supported by this service. All others should go through the VIES service.
+ if !strings.HasPrefix(vatNumber, "GB") {
+ return ErrInvalidCountryCode
+ }
+
+ response, err := http.Get(fmt.Sprintf(ukVATServiceURL, vatNumber[2:]))
+ if err != nil {
+ return ErrServiceUnavailable{Err: err}
+ }
+ defer func(Body io.ReadCloser) {
+ _ = Body.Close()
+ }(response.Body)
+
+ if response.StatusCode == http.StatusBadRequest {
+ return ErrInvalidVATNumberFormat
+ }
+ if response.StatusCode == http.StatusNotFound {
+ return ErrVATNumberNotFound
+ }
+ if response.StatusCode != http.StatusOK {
+ return ErrServiceUnavailable{
+ Err: fmt.Errorf("unexpected status code from UK VAT API: %d", response.StatusCode),
+ }
+ }
+
+ // If we receive a valid 200 response from this API, it means the VAT number exists and is valid
+ return nil
+}
+
+// API Documentation:
+// https://developer.service.hmrc.gov.uk/api-documentation/docs/api/service/vat-registered-companies-api/1.0
+const ukVATServiceURL = "https://api.service.hmrc.gov.uk/organisations/vat/check-vat-number/lookup/%s"
diff --git a/uk_vat_service_test.go b/uk_vat_service_test.go
new file mode 100644
index 0000000..3dd1bb2
--- /dev/null
+++ b/uk_vat_service_test.go
@@ -0,0 +1,27 @@
+//go:build integration
+// +build integration
+
+package vat
+
+import "testing"
+
+var ukTests = []struct {
+ vatNumber string
+ expectedError error
+}{
+ {"GB333289454", nil}, // valid number (as of 2024-04-26)
+ {"GB0472429986", ErrInvalidVATNumberFormat},
+ {"Hi", ErrInvalidCountryCode},
+ {"GB333289453", ErrVATNumberNotFound},
+}
+
+// TestUKVATService tests the UK VAT service. Just meant to be a quick way to check that this service is working.
+// Makes external calls that sometimes might fail. Do not include them in CI/CD.
+func TestUKVATService(t *testing.T) {
+ for _, test := range ukTests {
+ err := UKVATLookupService.Validate(test.vatNumber)
+ if err != test.expectedError {
+ t.Errorf("Expected <%v> for %v, got <%v>", test.expectedError, test.vatNumber, err)
+ }
+ }
+}
diff --git a/validate.go b/validate.go
new file mode 100644
index 0000000..7f49a85
--- /dev/null
+++ b/validate.go
@@ -0,0 +1,91 @@
+package vat
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+// Validate validates a VAT number by both format and existence. If no error then it is valid.
+func Validate(vatNumber string) error {
+ err := ValidateFormat(vatNumber)
+ if err != nil {
+ return err
+ }
+ return ValidateExists(vatNumber)
+}
+
+// ValidateFormat validates a VAT number by its format. If no error is returned then it is valid.
+func ValidateFormat(vatNumber string) error {
+ patterns := map[string]string{
+ "AT": `U[A-Z0-9]{8}`,
+ "BE": `(0[0-9]{9}|[0-9]{10})`,
+ "BG": `[0-9]{9,10}`,
+ "CH": `(?:E(?:-| )[0-9]{3}(?:\.| )[0-9]{3}(?:\.| )[0-9]{3}( MWST)?|E[0-9]{9}(?:MWST)?)`,
+ "CY": `[0-9]{8}[A-Z]`,
+ "CZ": `[0-9]{8,10}`,
+ "DE": `[0-9]{9}`,
+ "DK": `[0-9]{8}`,
+ "EE": `[0-9]{9}`,
+ "EL": `[0-9]{9}`,
+ "ES": `[A-Z][0-9]{7}[A-Z]|[0-9]{8}[A-Z]|[A-Z][0-9]{8}`,
+ "FI": `[0-9]{8}`,
+ "FR": `([A-Z]{2}|[0-9]{2})[0-9]{9}`,
+ // Supposedly the regex for GB numbers is `[0-9]{9}|[0-9]{12}|(GD|HA)[0-9]{3}`,
+ // but our validator service only accepts numbers with 9 or 12 digits following the country code.
+ // Seems like the official site only accepts 9 digits... https://www.gov.uk/check-uk-vat-number
+ "GB": `([0-9]{9}|[0-9]{12})`,
+ "HR": `[0-9]{11}`,
+ "HU": `[0-9]{8}`,
+ "IE": `[A-Z0-9]{7}[A-Z]|[A-Z0-9]{7}[A-W][A-I]`,
+ "IT": `[0-9]{11}`,
+ "LT": `([0-9]{9}|[0-9]{12})`,
+ "LU": `[0-9]{8}`,
+ "LV": `[0-9]{11}`,
+ "MT": `[0-9]{8}`,
+ "NL": `[0-9]{9}B[0-9]{2}`,
+ "PL": `[0-9]{10}`,
+ "PT": `[0-9]{9}`,
+ "RO": `[0-9]{2,10}`,
+ "SE": `[0-9]{12}`,
+ "SI": `[0-9]{8}`,
+ "SK": `[0-9]{10}`,
+ "XI": `([0-9]{9}|[0-9]{12})`, // Northern Ireland, same format as GB
+ }
+
+ if len(vatNumber) < 3 {
+ return ErrInvalidVATNumberFormat
+ }
+
+ vatNumber = strings.ToUpper(vatNumber)
+
+ pattern, ok := patterns[vatNumber[0:2]]
+ if !ok {
+ return ErrInvalidCountryCode
+ }
+
+ matched, err := regexp.MatchString(fmt.Sprintf("^%s$", pattern), vatNumber[2:])
+ if err != nil {
+ return err
+ }
+ if !matched {
+ return ErrInvalidVATNumberFormat
+ }
+ return nil
+}
+
+// ValidateExists validates that the given VAT number exists in the external lookup service.
+func ValidateExists(vatNumber string) error {
+ if len(vatNumber) < 3 {
+ return ErrInvalidVATNumberFormat
+ }
+
+ vatNumber = strings.ToUpper(vatNumber)
+
+ lookupService := ViesLookupService
+ if strings.HasPrefix(vatNumber, "GB") {
+ lookupService = UKVATLookupService
+ }
+
+ return lookupService.Validate(vatNumber)
+}
diff --git a/validate_test.go b/validate_test.go
new file mode 100644
index 0000000..1dbba86
--- /dev/null
+++ b/validate_test.go
@@ -0,0 +1,214 @@
+package vat
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/teamwork/vat/v3/mocks"
+)
+
+var tests = []struct {
+ number string
+ expectedError error
+}{
+ {"", ErrInvalidVATNumberFormat},
+ {"A", ErrInvalidVATNumberFormat},
+ {"AB123A01", ErrInvalidCountryCode},
+ {"ATU12345678", nil},
+ {"ATU15673009", nil},
+ {"ATU1234567", ErrInvalidVATNumberFormat},
+ {"BE0123456789", nil},
+ {"BE1234567891", nil},
+ {"BE0999999999", nil},
+ {"BE9999999999", nil},
+ {"BE012345678", ErrInvalidVATNumberFormat},
+ {"BE123456789", ErrInvalidVATNumberFormat},
+ {"BG123456789", nil},
+ {"BG1234567890", nil},
+ {"BG1234567", ErrInvalidVATNumberFormat},
+ {"CHE-156.730.098 MWST", nil},
+ {"CHE-156.730.098", nil},
+ {"CHE156730098MWST", nil},
+ {"CHE156730098", nil},
+ {"CY12345678X", nil},
+ {"CY15673009L", nil},
+ {"CY1234567X", ErrInvalidVATNumberFormat},
+ {"CZ12345678", nil},
+ {"CZ1234567", ErrInvalidVATNumberFormat},
+ {"DE123456789", nil},
+ {"DE12345678", ErrInvalidVATNumberFormat},
+ {"DK12345678", nil},
+ {"DK1234567", ErrInvalidVATNumberFormat},
+ {"EE123456789", nil},
+ {"EE12345678", ErrInvalidVATNumberFormat},
+ {"EL123456789", nil},
+ {"EL12345678", ErrInvalidVATNumberFormat},
+ {"ESX12345678", nil},
+ {"ESX1234567", ErrInvalidVATNumberFormat},
+ {"FI1234567", ErrInvalidVATNumberFormat},
+ {"FI12345678", nil},
+ {"FR12345678901", nil},
+ {"FR1234567890", ErrInvalidVATNumberFormat},
+ {"GB999999973", nil},
+ {"GB156730098481", nil},
+ {"GBGD549", ErrInvalidVATNumberFormat},
+ {"GBHA549", ErrInvalidVATNumberFormat},
+ {"GB99999997", ErrInvalidVATNumberFormat},
+ {"HU12345678", nil},
+ {"HU1234567", ErrInvalidVATNumberFormat},
+ {"HR12345678901", nil},
+ {"HR1234567890", ErrInvalidVATNumberFormat},
+ {"IE1234567X", nil},
+ {"IE123456X", ErrInvalidVATNumberFormat},
+ {"IT12345678901", nil},
+ {"IT1234567890", ErrInvalidVATNumberFormat},
+ {"LT123456789", nil},
+ {"LT12345678", ErrInvalidVATNumberFormat},
+ {"LU26375245", nil},
+ {"LU12345678", nil},
+ {"LU1234567", ErrInvalidVATNumberFormat},
+ {"LV12345678901", nil},
+ {"LV1234567890", ErrInvalidVATNumberFormat},
+ {"MT12345678", nil},
+ {"MT1234567", ErrInvalidVATNumberFormat},
+ {"NL123456789B01", nil},
+ {"NL123456789B12", nil},
+ {"NL12345678B12", ErrInvalidVATNumberFormat},
+ {"PL1234567890", nil},
+ {"PL123456789", ErrInvalidVATNumberFormat},
+ {"PT123456789", nil},
+ {"PT12345678", ErrInvalidVATNumberFormat},
+ {"RO123456789", nil},
+ {"RO1", ErrInvalidVATNumberFormat}, // Romania has a really weird VAT format...
+ {"SE123456789012", nil},
+ {"SE12345678901", ErrInvalidVATNumberFormat},
+ {"SI12345678", nil},
+ {"SI1234567", ErrInvalidVATNumberFormat},
+ {"SK1234567890", nil},
+ {"SK123456789", ErrInvalidVATNumberFormat},
+ {"KL123456789B12", ErrInvalidCountryCode},
+ {"NL12343678B12", ErrInvalidVATNumberFormat},
+ {"PL1224567890", nil},
+ {"PL123456989", ErrInvalidVATNumberFormat},
+ {"PT125456789", nil},
+ {"PT16345678", ErrInvalidVATNumberFormat},
+ {"RO123456789", nil},
+ {"KT123456789", ErrInvalidCountryCode},
+ {"LT12335678", ErrInvalidVATNumberFormat},
+ {"LU26275245", nil},
+ {"LU14345678", nil},
+ {"LU1234567", ErrInvalidVATNumberFormat},
+ {"LQ12345678901", ErrInvalidCountryCode},
+ {"QE123456789", ErrInvalidCountryCode},
+ {"DE12375670", ErrInvalidVATNumberFormat},
+ {"DK123365678", ErrInvalidVATNumberFormat},
+ {"DO1231567", ErrInvalidCountryCode},
+ {"EE123456789", nil},
+ {"EL123434678", nil},
+ {"EL122456789", nil},
+ {"EL12245678", ErrInvalidVATNumberFormat},
+ {"CY1134567X", ErrInvalidVATNumberFormat},
+ {"CZ17345678", nil},
+ {"CZ1239567", ErrInvalidVATNumberFormat},
+ {"DE123456789", nil},
+ {"DE12325678", ErrInvalidVATNumberFormat},
+ {"DK12385678", nil},
+ {"DK1234767", ErrInvalidVATNumberFormat},
+ {"EE123456689", nil},
+ {"EE12343678", ErrInvalidVATNumberFormat},
+ {"EL123426789", nil},
+ {"EL12342678", ErrInvalidVATNumberFormat},
+ {"ESX12315678", nil},
+ {"ESX1634567", ErrInvalidVATNumberFormat},
+ {"FI1274567", ErrInvalidVATNumberFormat},
+ {"FI12385678", nil},
+ {"FR12349678901", nil},
+ {"FR1234597890", ErrInvalidVATNumberFormat},
+ {"GB999979973", nil},
+ {"GB156730984812", nil},
+ {"GBGD529", ErrInvalidVATNumberFormat},
+ {"GBHA519", ErrInvalidVATNumberFormat},
+ {"GB99997997", ErrInvalidVATNumberFormat},
+ {"GB15673098481", ErrInvalidVATNumberFormat},
+ {"HU12342678", nil},
+ {"HU1234167", ErrInvalidVATNumberFormat},
+ {"HR12341678901", nil},
+ {"HR1234267890", ErrInvalidVATNumberFormat},
+ {"IE1234167X", nil},
+ {"IE123356X", ErrInvalidVATNumberFormat},
+ {"IT12245678901", nil},
+ {"IT122456789013333", ErrInvalidVATNumberFormat},
+ {"IT1231567890", ErrInvalidVATNumberFormat},
+ {"LT123156789", nil},
+ {"LT12343678", ErrInvalidVATNumberFormat},
+ {"LU26371245", nil},
+ {"LU12325678", nil},
+ {"LU1214567", ErrInvalidVATNumberFormat},
+ {"LV12345578901", nil},
+ {"LV1234367890", ErrInvalidVATNumberFormat},
+ {"MT12325678", nil},
+ {"MT1234167", ErrInvalidVATNumberFormat},
+ {"NL123156789B01", nil},
+ {"NL113456789B12", nil},
+ {"NL12315678B12", ErrInvalidVATNumberFormat},
+ {"PO1234567890", ErrInvalidCountryCode},
+}
+
+func BenchmarkValidateFormat(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _ = ValidateFormat("NL" + strconv.Itoa(i))
+ }
+}
+
+func TestValidateFormat(t *testing.T) {
+ for _, test := range tests {
+ err := ValidateFormat(test.number)
+ if err != test.expectedError {
+ t.Errorf("Expected <%v> for %v, got <%v>", test.expectedError, test.number, err)
+ }
+ }
+}
+
+func TestValidateExists(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockViesService := mocks.NewMockLookupServiceInterface(ctrl)
+ mockUKVATService := mocks.NewMockLookupServiceInterface(ctrl)
+ ViesLookupService = mockViesService
+ UKVATLookupService = mockUKVATService
+
+ defer restoreLookupServices()
+
+ var lookupTests = []struct {
+ vatNumber string
+ service *mocks.MockLookupServiceInterface
+ expectedError error
+ }{
+ {"BE0472429986", mockViesService, ErrVATNumberNotFound},
+ {"NL123456789B01", mockViesService, nil},
+ {"Hi", mockViesService, ErrInvalidVATNumberFormat},
+ {"INVALID INPUT", mockViesService, ErrInvalidVATNumberFormat},
+ {"BE0472429986", mockViesService, ErrServiceUnavailable{Err: nil}},
+ {"GB333289454", mockUKVATService, nil},
+ // XI is Northern Ireland, which while part of the UK is actually still validated by VIES
+ {"XI0472429986", mockViesService, nil},
+ }
+
+ for _, test := range lookupTests {
+ if len(test.vatNumber) >= 3 {
+ test.service.EXPECT().Validate(test.vatNumber).Return(test.expectedError)
+ }
+
+ err := ValidateExists(test.vatNumber)
+ if err != test.expectedError {
+ t.Errorf("Expected <%v> for %v, got <%v>", test.expectedError, test.vatNumber, err)
+ }
+ }
+}
+
+func restoreLookupServices() {
+ ViesLookupService = &viesService{}
+ UKVATLookupService = &ukVATService{}
+}
diff --git a/vat.go b/vat.go
index 19613ee..730d603 100644
--- a/vat.go
+++ b/vat.go
@@ -2,10 +2,11 @@
Package vat helps you deal with European VAT in Go.
It offers VAT number validation using the VIES VAT validation API & VAT rates retrieval using jsonvat.com
+It also offers UK VAT number validation using a UK VAT API.
Validate a VAT number
- validity := vat.ValidateNumber("NL123456789B01")
+ err := vat.Validate("NL123456789B01")
Get VAT rate that is currently in effect for a given country
@@ -13,3 +14,9 @@ Get VAT rate that is currently in effect for a given country
r, _ := c.GetRate("standard")
*/
package vat
+
+// ViesLookupService is the interface for the VIES VAT number validation service
+var ViesLookupService LookupServiceInterface = &viesService{}
+
+// UKVATLookupService is the interface for the UK VAT number validation service
+var UKVATLookupService LookupServiceInterface = &ukVATService{}
diff --git a/vies_service.go b/vies_service.go
new file mode 100644
index 0000000..0c4eb0f
--- /dev/null
+++ b/vies_service.go
@@ -0,0 +1,127 @@
+package vat
+
+import (
+ "bytes"
+ "encoding/xml"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+)
+
+// LookupServiceInterface is an interface for the service that calls external services to validate VATs.
+type LookupServiceInterface interface {
+ Validate(vatNumber string) error
+}
+
+// viesService validates EU VAT numbers with the VIES service
+type viesService struct{}
+
+// Validate returns whether the given VAT number is valid or not
+func (s *viesService) Validate(vatNumber string) error {
+ if len(vatNumber) < 3 {
+ return ErrInvalidVATNumberFormat
+ }
+
+ res, err := s.lookup(s.getEnvelope(vatNumber))
+ if err != nil {
+ return ErrServiceUnavailable{Err: err}
+ }
+ defer func() {
+ _ = res.Body.Close()
+ }()
+
+ xmlRes, err := io.ReadAll(res.Body)
+ if err != nil {
+ return ErrServiceUnavailable{Err: err} // assume if we can't read the body then VIES gave us a bad response
+ }
+
+ // check if response contains "INVALID_INPUT" string
+ if bytes.Contains(xmlRes, []byte("INVALID_INPUT")) {
+ return ErrInvalidVATNumberFormat
+ }
+
+ // check if response contains "MS_UNAVAILABLE" string
+ if bytes.Contains(xmlRes, []byte("MS_UNAVAILABLE")) ||
+ bytes.Contains(xmlRes, []byte("MS_MAX_CONCURRENT_REQ")) {
+ return ErrServiceUnavailable{Err: nil}
+ }
+
+ var rd struct {
+ XMLName xml.Name `xml:"Envelope"`
+ Soap struct {
+ XMLName xml.Name `xml:"Body"`
+ Soap struct {
+ XMLName xml.Name `xml:"checkVatResponse"`
+ CountryCode string `xml:"countryCode"`
+ VATNumber string `xml:"vatNumber"`
+ RequestDate string `xml:"requestDate"` // 2015-03-06+01:00
+ Valid bool `xml:"valid"`
+ Name string `xml:"name"`
+ Address string `xml:"address"`
+ }
+ }
+ }
+ if err = xml.Unmarshal(xmlRes, &rd); err != nil {
+ return ErrServiceUnavailable{Err: err} // assume if response data doesn't match the struct, the service is down
+ }
+
+ r := &viesResponse{
+ CountryCode: rd.Soap.Soap.CountryCode,
+ VATNumber: rd.Soap.Soap.VATNumber,
+ RequestDate: rd.Soap.Soap.RequestDate,
+ Valid: rd.Soap.Soap.Valid,
+ Name: rd.Soap.Soap.Name,
+ Address: rd.Soap.Soap.Address,
+ }
+
+ if !r.Valid {
+ return ErrVATNumberNotFound
+ }
+ return nil
+}
+
+// getEnvelope parses VIES lookup envelope template
+func (s *viesService) getEnvelope(n string) string {
+ n = strings.ToUpper(n)
+ countryCode := n[0:2]
+ vatNumber := n[2:]
+ const envelopeTemplate = `
+
+
+
+ {{.countryCode}}
+ {{.vatNumber}}
+
+
+`
+
+ e := envelopeTemplate
+ e = strings.Replace(e, "{{.countryCode}}", countryCode, 1)
+ e = strings.Replace(e, "{{.vatNumber}}", vatNumber, 1)
+ return e
+}
+
+// lookup calls the VIES service to get info about the VAT number
+func (s *viesService) lookup(envelope string) (*http.Response, error) {
+ envelopeBuffer := bytes.NewBufferString(envelope)
+ client := http.Client{
+ Timeout: time.Duration(ViesServiceTimeout) * time.Second,
+ }
+ return client.Post(viesServiceURL, "text/xml;charset=UTF-8", envelopeBuffer)
+}
+
+const viesServiceURL = "https://ec.europa.eu/taxation_customs/vies/services/checkVatService"
+
+// ViesServiceTimeout is the timeout for the VIES service
+const ViesServiceTimeout = 10
+
+// viesResponse holds the response data from the Vies call
+type viesResponse struct {
+ CountryCode string
+ VATNumber string
+ RequestDate string
+ Valid bool
+ Name string
+ Address string
+}
diff --git a/vies_service_test.go b/vies_service_test.go
new file mode 100644
index 0000000..99b42de
--- /dev/null
+++ b/vies_service_test.go
@@ -0,0 +1,27 @@
+//go:build integration
+// +build integration
+
+package vat
+
+import "testing"
+
+var viesTests = []struct {
+ vatNumber string
+ expectedError error
+}{
+ {"BE0472429986", nil}, // valid number
+ {"NL123456789B01", ErrVATNumberNotFound},
+ {"Hi", ErrInvalidVATNumberFormat},
+ {"INVALID INPUT", ErrInvalidVATNumberFormat},
+}
+
+// TestViesService tests the VIES service. Just meant to be a quick way to check that this service is working.
+// The external VIES calls are not always reliable so sometimes these tests may fail. Do not include them in CI/CD.
+func TestViesService(t *testing.T) {
+ for _, test := range viesTests {
+ err := ViesLookupService.Validate(test.vatNumber)
+ if err != test.expectedError {
+ t.Errorf("Expected <%v> for %v, got <%v>", test.expectedError, test.vatNumber, err)
+ }
+ }
+}