-
Notifications
You must be signed in to change notification settings - Fork 0
/
vies.go
179 lines (154 loc) · 5.07 KB
/
vies.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package vies
import (
"bytes"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"time"
)
const (
// ProductionEndpoint is the production endpoint for the VIES VAT service.
ProductionEndpoint string = "http://ec.europa.eu/taxation_customs/vies/services/checkVatService"
// TestEndpoint is the test endpoint for the VIES VAT service.
TestEndpoint string = "http://ec.europa.eu/taxation_customs/vies/services/checkVatTestService"
)
type service struct {
endpoint string
httpClient *http.Client
}
// NewService creates a VIES service with a default timeout of 10 seconds for the HTTP request.
// You can pass a custom endpoint for testing purposes or proxying, or use vies.ProductionEndpoint
// or vies.TestEndpoint to reach the VIES VAT service.
func NewService(endpoint string) *service {
return &service{
endpoint: endpoint,
httpClient: &http.Client{
Timeout: time.Second * 10,
},
}
}
// NewServiceWithTimeout creates a VIES service with the specified timeout for the HTTP request.
// You can pass a custom endpoint for testing purposes or proxying, or use vies.ProductionEndpoint
// or vies.TestEndpoint to reach the VIES VAT service.
func NewServiceWithTimeout(endpoint string, timeout time.Duration) *service {
return &service{
endpoint: endpoint,
httpClient: &http.Client{
Timeout: timeout,
},
}
}
type vatData struct {
CountryCode string
VatNumber string
Valid bool
Name string
Address string
}
// CheckVAT given a VAT number.
// This will infer the country based on the two initial digits from the VAT number.
//
// It will not necessarily return a non-nil error if the VAT is not valid.
//
// Example:
// svc := vies.NewService(vies.TestEndpoint)
// vat, err := svc.CheckVAT("NL123456789B01")
// if err != nil {
// return
// }
// fmt.Println("Valid?:", vat.Valid)
func (s *service) CheckVAT(fullVatNumber string) (*vatData, error) {
if len(fullVatNumber) <= 2 {
return nil, ErrInvalidInput
}
countryCode := fullVatNumber[0:2]
vatNumber := fullVatNumber[2:]
return s.checkVat(countryCode, vatNumber)
}
// CheckVAT given a VAT number and a country.
//
// The VAT number can either contain the country code or not, as long as it matches with
// the countryCode provided.
// If you want to use the country inside the VAT number, consider using CheckVAT instead.
//
// It will not necessarily return a non-nil error if the VAT is not valid.
//
// Example:
// svc := vies.NewService(vies.TestEndpoint)
// vat, err := svc.CheckVATWithCountry("NL", "123456789B01")
// if err != nil {
// return
// }
// fmt.Println("Valid?:", vat.Valid)
func (s *service) CheckVATWithCountry(countryCode, vatNumber string) (*vatData, error) {
if len(vatNumber) <= 2 {
return nil, ErrInvalidInput
}
if countryCode[0] == vatNumber[0] && countryCode[1] == vatNumber[1] {
return s.checkVat(countryCode, vatNumber[2:])
}
return s.checkVat(countryCode, vatNumber)
}
func (s *service) checkVat(countryCode, vatNumber string) (*vatData, error) {
if _, ok := validCountries[countryCode]; !ok {
return nil, ErrInvalidInput
}
envelope, err := s.request(countryCode, vatNumber)
if err != nil {
return nil, err
}
return &vatData{
CountryCode: envelope.Body.CheckVatResponse.CountryCode,
VatNumber: envelope.Body.CheckVatResponse.VatNumber,
Valid: envelope.Body.CheckVatResponse.Valid,
Name: envelope.Body.CheckVatResponse.Name,
Address: envelope.Body.CheckVatResponse.Address,
}, nil
}
type transportResponse struct {
Body struct {
CheckVatResponse struct {
CountryCode string `xml:"countryCode"`
VatNumber string `xml:"vatNumber"`
Valid bool `xml:"valid"`
Name string `xml:"name"`
Address string `xml:"address"`
} `xml:"checkVatResponse"`
Fault struct {
Message string `xml:"faultstring"`
} `xml:"Fault"`
} `xml:"Body"`
}
const requestBody string = `<s11:Envelope xmlns:s11="http://schemas.xmlsoap.org/soap/envelope/"><s11:Body><tns1:checkVat xmlns:tns1="urn:ec.europa.eu:taxud:vies:services:checkVat:types"><tns1:countryCode>%s</tns1:countryCode><tns1:vatNumber>%s</tns1:vatNumber></tns1:checkVat></s11:Body></s11:Envelope>`
func (s *service) request(countryCode, vatNumber string) (*transportResponse, error) {
reqBody := fmt.Sprintf(requestBody, countryCode, vatNumber)
req, err := http.NewRequest(http.MethodPost, s.endpoint, bytes.NewBufferString(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "text/xml")
req.Header.Add("Content-Type", "charset=utf-8")
req.Header.Set("SOAPAction", "checkVat")
res, err := s.httpClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
var envelope transportResponse
if err := xml.Unmarshal([]byte(resBody), &envelope); err != nil {
return nil, err
}
if envelope.Body.Fault.Message != "" {
sentinelError, ok := toSentinelError[envelope.Body.Fault.Message]
if !ok {
sentinelError = ErrServiceUnavailable
}
return nil, fmt.Errorf("request failed with code %w", sentinelError)
}
return &envelope, nil
}